gentle-pi 4.2.0 → 4.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/README.md +336 -137
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/orchestrator-delegation-reference.md +163 -0
  6. package/assets/orchestrator-delegation.md +25 -176
  7. package/assets/orchestrator-tracking.md +1 -1
  8. package/assets/orchestrator-verification.md +14 -8
  9. package/assets/orchestrator-writer.md +5 -1
  10. package/assets/orchestrator.md +9 -9
  11. package/bin/gentle-shell.mjs +170 -22
  12. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  13. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  14. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  15. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  16. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  17. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  18. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  19. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  20. package/docs/assets/diagrams/measured-routing.svg +49 -0
  21. package/docs/assets/diagrams/native-review.svg +52 -12
  22. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  23. package/docs/assets/diagrams/permission-modes.svg +51 -0
  24. package/docs/assets/features/audio-notifications.png +0 -0
  25. package/docs/assets/features/command-palette.png +0 -0
  26. package/docs/bundled-install.md +33 -3
  27. package/docs/delegated-verification.md +10 -8
  28. package/docs/gentle-agents-activity.md +11 -5
  29. package/docs/gentle-shell-uninstall.md +8 -1
  30. package/docs/gentle-shell.md +5 -4
  31. package/docs/install-wizard.md +8 -1
  32. package/docs/permission-modes.md +1 -1
  33. package/docs/readme-reference.md +107 -19
  34. package/docs/yolo-mode.md +21 -8
  35. package/extensions/codegraph-tools.ts +68 -6
  36. package/extensions/gentle-agents.ts +99 -12
  37. package/extensions/gentle-ai.ts +284 -52
  38. package/extensions/gentle-herdr-activity.ts +2 -3
  39. package/extensions/gentle-shell.ts +27 -12
  40. package/extensions/gentle-todo.ts +43 -9
  41. package/extensions/quiet-tools.ts +1 -1
  42. package/extensions/session-scratch.ts +365 -0
  43. package/extensions/startup-banner.ts +2 -3
  44. package/lib/agent-home.ts +8 -3
  45. package/lib/agents-protocol.ts +3 -0
  46. package/lib/agents-runner.ts +55 -6
  47. package/lib/agents-view.ts +3 -1
  48. package/lib/bounded-writer-admission.ts +51 -0
  49. package/lib/command-palette-catalog.ts +0 -4
  50. package/lib/commit-slice-signal.ts +100 -0
  51. package/lib/context-backstop.ts +84 -0
  52. package/lib/delivered-test-lint.ts +80 -0
  53. package/lib/destructive-command-guard.ts +120 -9
  54. package/lib/gauge-theme.ts +146 -0
  55. package/lib/gentle-ai-binary.ts +28 -7
  56. package/lib/gentle-shell-bundled.ts +39 -3
  57. package/lib/gentle-shell-import-pi.ts +233 -0
  58. package/lib/gentle-shell-launcher.ts +145 -11
  59. package/lib/gentle-shell-uninstall.ts +18 -1
  60. package/lib/herdr-activity.ts +24 -0
  61. package/lib/native-review-cli.ts +9 -1
  62. package/lib/orchestrator-discovery.ts +5 -1
  63. package/lib/orchestrator-presence.ts +9 -0
  64. package/lib/orchestrator-scope.ts +21 -3
  65. package/lib/provider-contract-bundle.ts +99 -53
  66. package/lib/review-candidate-view-owner.ts +29 -19
  67. package/lib/review-candidate-view.ts +36 -2
  68. package/lib/review-risk-assessment.ts +43 -38
  69. package/lib/review-session-standing-permission.ts +3 -1
  70. package/lib/session-profile-freeze.ts +30 -1
  71. package/lib/session-scratch.ts +474 -0
  72. package/lib/shell-bar.ts +25 -25
  73. package/lib/shell-changes-view.ts +80 -13
  74. package/lib/shell-gauge.ts +49 -6
  75. package/lib/shell-todo.ts +23 -4
  76. package/lib/shell-usage-view.ts +5 -1
  77. package/lib/shell-usage.ts +7 -7
  78. package/lib/verifier-admission.ts +29 -0
  79. package/lib/yolo-temp-scratch.ts +69 -0
  80. package/package.json +1 -2
  81. package/runtime/gentle-ai-binary.mjs +28 -7
  82. package/runtime/gentle-shell-bundled.mjs +38 -2
  83. package/runtime/gentle-shell-import-pi.mjs +234 -0
  84. package/runtime/gentle-shell-launcher.mjs +143 -9
  85. package/runtime/gentle-shell-uninstall.mjs +18 -1
  86. package/runtime/native-review-cli.mjs +8 -0
  87. package/runtime/review-risk-assessment.mjs +43 -38
  88. package/scripts/build-runtime-modules.mjs +1 -0
  89. package/scripts/bundled-engram-migration.mjs +125 -0
  90. package/scripts/gentle-ai-installer.mjs +10 -10
  91. package/scripts/installer-server.mjs +14 -4
  92. package/scripts/main-channel.mjs +3 -2
  93. package/scripts/run-test-suite.mjs +5 -0
  94. package/scripts/verify-package-files.mjs +19 -13
  95. package/skills/gentle-ai/SKILL.md +3 -3
  96. package/skills/work-unit-commits/SKILL.md +5 -3
  97. package/tests/agent-risk-escalation.test.ts +37 -10
  98. package/tests/agents-runner.test.ts +260 -3
  99. package/tests/autonomous-guard.test.ts +122 -0
  100. package/tests/bounded-writer-admission.test.ts +11 -3
  101. package/tests/card-content-policy.test.ts +19 -9
  102. package/tests/child-safety.test.ts +39 -0
  103. package/tests/codegraph-tools.test.ts +123 -2
  104. package/tests/command-palette.test.ts +14 -4
  105. package/tests/commit-slice-signal.test.ts +162 -0
  106. package/tests/context-backstop.test.ts +159 -0
  107. package/tests/delegate-for-reason-contract.test.ts +91 -24
  108. package/tests/delivered-test-lint.test.ts +105 -0
  109. package/tests/destructive-command-guard.test.ts +85 -1
  110. package/tests/engram-safety.test.ts +1 -1
  111. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  112. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  113. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  114. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  115. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  116. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  117. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  118. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  119. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  120. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  121. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  122. package/tests/gauge-theme.test.ts +120 -0
  123. package/tests/gentle-agents.test.ts +282 -3
  124. package/tests/gentle-ai-binary.test.ts +1 -1
  125. package/tests/gentle-ai-child-guards.test.ts +61 -0
  126. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  127. package/tests/gentle-ai-installer.test.ts +51 -51
  128. package/tests/gentle-shell-bin.test.ts +163 -0
  129. package/tests/gentle-shell-bundled.test.ts +169 -6
  130. package/tests/gentle-shell-import-pi.test.ts +262 -0
  131. package/tests/gentle-shell-launcher.test.ts +79 -0
  132. package/tests/gentle-shell-uninstall.test.ts +30 -0
  133. package/tests/gentle-shell.test.ts +42 -9
  134. package/tests/gentle-todo.test.ts +179 -0
  135. package/tests/headless-delivery-question.test.ts +58 -0
  136. package/tests/herdr-activity.test.ts +34 -1
  137. package/tests/installer-server.test.ts +10 -2
  138. package/tests/installer-windows-bootstrap.test.ts +33 -4
  139. package/tests/native-review-capability-contract.test.ts +7 -1
  140. package/tests/native-review-parity.test.ts +22 -1
  141. package/tests/no-self-pause-contract.test.ts +5 -2
  142. package/tests/odd-integration.test.ts +5 -1
  143. package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
  144. package/tests/odd-routing-contract.test.ts +115 -30
  145. package/tests/orchestrator-budget.test.ts +19 -8
  146. package/tests/orchestrator-discovery.test.ts +29 -0
  147. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  148. package/tests/orchestrator-scope.test.ts +66 -0
  149. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  150. package/tests/package-manifest.test.ts +24 -20
  151. package/tests/pi-update-launcher.test.ts +21 -2
  152. package/tests/primary-session-prompt.test.ts +101 -0
  153. package/tests/provider-contract-bundle.test.ts +118 -0
  154. package/tests/provider-contract-mirror.test.ts +24 -0
  155. package/tests/quiet-tool-rendering.test.ts +23 -0
  156. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  157. package/tests/rdd-status-line.test.ts +79 -0
  158. package/tests/review-agent-end-preflight.test.ts +6 -0
  159. package/tests/review-candidate-view.test.ts +49 -0
  160. package/tests/review-contract-prompt.test.ts +5 -2
  161. package/tests/review-controller-native-routing.test.ts +14 -1
  162. package/tests/review-controller.test.ts +1 -1
  163. package/tests/review-host-relay-routing.test.ts +59 -2
  164. package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
  165. package/tests/review-owner-identity.test.ts +132 -0
  166. package/tests/review-risk-assessment.test.ts +163 -76
  167. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  168. package/tests/review-session-standing-permission.test.ts +27 -1
  169. package/tests/run-test-suite.test.ts +15 -0
  170. package/tests/runtime-harness.mjs +16 -6
  171. package/tests/session-changes-shell.test.ts +1 -1
  172. package/tests/session-profile-freeze.test.ts +23 -0
  173. package/tests/session-scratch.test.ts +945 -0
  174. package/tests/session-subject-routing-contract.test.ts +2 -1
  175. package/tests/shell-bar.test.ts +7 -0
  176. package/tests/shell-changes-view.test.ts +137 -1
  177. package/tests/shell-gauge.test.ts +42 -0
  178. package/tests/small-task-inline-contract.test.ts +37 -0
  179. package/tests/standalone-config-home.test.ts +324 -0
  180. package/tests/startup-banner.test.ts +5 -4
  181. package/tests/startup-home-repo.test.ts +34 -0
  182. package/tests/support/orchestrator-modules.ts +1 -0
  183. package/tests/task-size-routing-contract.test.ts +69 -9
  184. package/tests/test-scratch-contract.test.ts +102 -0
  185. package/tests/test-strength-contract.test.ts +36 -1
  186. package/tests/tool-batching-contract.test.ts +64 -0
  187. package/tests/verification-proportionality.test.ts +46 -0
  188. package/tests/verifier-admission.test.ts +137 -0
  189. package/tests/verify-agent-contract.test.ts +8 -8
  190. package/tests/windows-hidden-processes.test.ts +5 -0
  191. package/tests/writer-edit-surface-scope.test.ts +3 -0
  192. package/tests/writer-feature-document-admission.test.ts +156 -0
  193. package/tests/yolo-mode.test.ts +18 -0
  194. package/tests/yolo-temp-scratch.test.ts +71 -0
  195. package/scripts/mirror-odd-routing.mjs +0 -242
  196. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  197. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  198. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  199. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  200. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  201. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  202. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -1,8 +1,13 @@
1
1
  import { appendSystemPromptOnce } from "../lib/append-system-prompt.ts";
2
2
  import { dataArgumentRanges, recognizeDestructiveCommands } from "../lib/destructive-command-guard.ts";
3
+ import { defaultTempScratchFs, isTempScratchRemoval, type TempScratchFs } from "../lib/yolo-temp-scratch.ts";
3
4
  import { SHELL_COMMAND_TOOLS } from "../lib/background-jobs.ts";
5
+ import { evaluateDeliveredTestTool } from "../lib/delivered-test-lint.ts";
6
+ import { commitSliceToolResult } from "../lib/commit-slice-signal.ts";
7
+ import { appendContextBackstopNotice, createContextBackstopSignal } from "../lib/context-backstop.ts";
4
8
  import { blockChildDestructiveCommand } from "./child-safety.ts";
5
- import { allowedEditSurfaces as hasTaskScopedAllowedEditSurfaces, bindSessionRepositoryPreparation, captureBoundSessionRepositoryAuthority, isHomeOrBroadUserDirectory, prepareBoundSessionRepository, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sourcePathWithinProject } from "../lib/bounded-writer-admission.ts";
9
+ import { allowedEditSurfaces as hasTaskScopedAllowedEditSurfaces, bindSessionRepositoryPreparation, captureBoundSessionRepositoryAuthority, isHomeOrBroadUserDirectory, prepareBoundSessionRepository, rejectUndocumentedWriterLaunch, rejectUnscopedBoundedWriterDispatch, safeBootstrapDirectory, sourcePathWithinProject } from "../lib/bounded-writer-admission.ts";
10
+ import { rejectUnriskedVerifierLaunch } from "../lib/verifier-admission.ts";
6
11
  import { consumeReviewMutation, pendingReviewMutation, pendingReviewMutationProfiles, recordReviewMutation, type ReceiptSession } from "../lib/review-reminder-receipt.ts";
7
12
  import { createReviewSidebarPublisher } from "../lib/review-sidebar-state.ts";
8
13
  import { StatusTimingDiagnostics } from "../lib/status-timing-diagnostics.ts";
@@ -20,6 +25,7 @@ import {
20
25
  readdirSync,
21
26
  readFileSync,
22
27
  realpathSync,
28
+ statSync,
23
29
  writeFileSync,
24
30
  } from "node:fs";
25
31
  import {
@@ -38,6 +44,8 @@ import type {
38
44
  Theme,
39
45
  ThemeColor,
40
46
  ToolCallEventResult,
47
+ ToolResultEvent,
48
+ ToolResultEventResult,
41
49
  } from "@earendil-works/pi-coding-agent";
42
50
  import { Key, Text, isKeyRelease, matchesKey, truncateToWidth, type KeybindingsManager, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
43
51
  import { resolveGentlePiAgentHome, gentlePiConfigHome } from "../lib/agent-home.ts";
@@ -222,7 +230,7 @@ import {
222
230
  isSmallWriterProfile,
223
231
  resolveWriterProfile,
224
232
  decodeAgentRiskEscalation,
225
- escalatedRisk,
233
+ verificationRisk,
226
234
  HIGH_RISK_ITEMS,
227
235
  RDD_LINE,
228
236
  WRITER_PROFILE,
@@ -264,6 +272,8 @@ import {
264
272
  sameReviewSessionIdentity,
265
273
  type ReviewSessionIdentity,
266
274
  } from "../lib/review-session-standing-permission.ts";
275
+ import { herdrSetupHint } from "../lib/herdr-activity.ts";
276
+ import { isInteractiveMode } from "../lib/rpc-host.ts";
267
277
 
268
278
  const GRAPH_V1_ORDINARY_READ_ONLY = "Graph-v1 ordinary review authority is read-only; use native compact-v2 review operations";
269
279
  const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
@@ -788,6 +798,7 @@ const rddStatusMemo = new Map<string, { readonly status: NativeReviewModeStatus
788
798
  /** @internal test seam: clears the per-cwd RDD status memo. */
789
799
  function clearRddStatusMemoForTesting(): void {
790
800
  rddStatusMemo.clear();
801
+ rddStatusUnavailableWarned = false;
791
802
  }
792
803
 
793
804
  // gentle-pi#668 (corrected): last-known outcome for ONE candidate, keyed by
@@ -852,29 +863,62 @@ async function readRddModeStatusOnce(
852
863
  cwd: string,
853
864
  signal?: AbortSignal,
854
865
  ): Promise<NativeReviewModeStatus | undefined> {
855
- if (!nativeReviewCli?.reviewMode) return undefined;
866
+ const read = await readRddModeStatusWithCause(nativeReviewCli, cwd, signal);
867
+ return "status" in read ? read.status : undefined;
868
+ }
869
+
870
+ // gentle-shell#2250: the same read, keeping why it failed so the one-shot
871
+ // unavailable notice can tell a timeout from a native refusal (for example a
872
+ // repository on a network share that Git or gentle-ai rejects by ownership).
873
+ async function readRddModeStatusWithCause(
874
+ nativeReviewCli: Pick<NativeReviewCli, "reviewMode"> | null | undefined,
875
+ cwd: string,
876
+ signal?: AbortSignal,
877
+ ): Promise<{ readonly status: NativeReviewModeStatus } | { readonly cause: string }> {
878
+ if (!nativeReviewCli?.reviewMode) return { cause: "the native gentle-ai review CLI is not available" };
856
879
  try {
857
880
  const call = nativeReviewCli.reviewMode({ cwd, operation: NATIVE_REVIEW_MODE_OPERATION.STATUS, signal });
858
881
  const result = signal === undefined ? await call : await Promise.race([call, rddAbortRejection(signal)]);
859
- return result.status;
860
- } catch {
861
- return undefined;
882
+ return { status: result.status };
883
+ } catch (error) {
884
+ return { cause: describeRddStatusFailure(error, signal) };
862
885
  }
863
886
  }
864
887
 
888
+ const RDD_STATUS_CAUSE_LIMIT = 300;
889
+
890
+ function describeRddStatusFailure(error: unknown, signal: AbortSignal | undefined): string {
891
+ const command = "native `gentle-ai review mode status`";
892
+ if (signal?.aborted || (error instanceof NativeReviewCliError && error.code === NATIVE_REVIEW_ERROR_CODE.TIMEOUT)) {
893
+ return `${command} did not answer within ${RDD_STATUS_TIMEOUT_MS} ms`;
894
+ }
895
+ if (error instanceof NativeReviewCliError) {
896
+ const exit = error.diagnostics.exit_code === undefined ? "" : `, exit ${error.diagnostics.exit_code}`;
897
+ const detail = (error.diagnostics.stderr ?? "").replace(/\s+/g, " ").trim() || error.message;
898
+ return `${command} failed (${error.code}${exit}): ${boundRddStatusCause(detail)}`;
899
+ }
900
+ return boundRddStatusCause(error instanceof Error ? error.message : String(error));
901
+ }
902
+
903
+ function boundRddStatusCause(text: string): string {
904
+ const trimmed = text.replace(/[.\s]+$/, "");
905
+ return trimmed.length <= RDD_STATUS_CAUSE_LIMIT ? trimmed : `${trimmed.slice(0, RDD_STATUS_CAUSE_LIMIT - 1)}…`;
906
+ }
907
+
865
908
  // gentle-pi#662: read-only combined native risk assessment plus the computed
866
909
  // verification plan (`lib/review-risk-assessment.ts`), for the `gentle_review`
867
910
  // tool's `assess` operation. Never throws: an unavailable/failed native
868
911
  // assess call (older binary without the verb, timeout, malformed response)
869
- // resolves to the `unassessable` tier, which `verificationPlan` treats the
870
- // same as `high` -- the fail-closed rule from gentle-pi#662.
912
+ // resolves to the `unassessable` native tier, which informs only: like a
913
+ // native high, it adds no independent verifier without an agent escalation.
871
914
  interface ReviewAssessmentPlanDetails {
872
915
  schema: "gentle-pi.review-assessment-plan/v1";
873
916
  risk: VerificationTier;
874
- // gentle-shell#1494: the native tier before any agent escalation. `risk`
875
- // differs from it only when the agent raised the candidate to high.
917
+ // gentle-shell#1494: the native tier before any agent escalation. `risk` is
918
+ // the plan tier: high only through an escalation, else passive or medium.
876
919
  nativeRisk: VerificationTier;
877
- // `applied` is false when the native tier was already high or unassessable.
920
+ // `applied` is true whenever an escalation is present: without it the plan
921
+ // would be passive or medium.
878
922
  agentEscalation?: { item: number; label: string; reason: string; applied: boolean };
879
923
  reasons: readonly ReviewAssessmentReason[];
880
924
  changedPaths: number;
@@ -1031,9 +1075,10 @@ async function resolveReviewAssessmentPlan(
1031
1075
  }
1032
1076
 
1033
1077
  const nativeRisk: VerificationTier = assessment?.risk ?? VERIFICATION_TIER.UNASSESSABLE;
1034
- // gentle-shell#1494: an agent escalation raises passive/medium to high and
1035
- // never lowers; the native tier stays visible as `nativeRisk`.
1036
- const risk = escalatedRisk(nativeRisk, input.escalate);
1078
+ // gentle-shell#1494: the plan is high only through an agent escalation
1079
+ // naming a Task Size high-risk item; a native high or unassessable tier
1080
+ // informs as medium. The native tier stays visible as `nativeRisk`.
1081
+ const risk = verificationRisk(nativeRisk, input.escalate);
1037
1082
  // gentle-pi#668: the memo is read only for THIS candidate's own target
1038
1083
  // identity, never repository-only. It is still needed when the caller
1039
1084
  // claims `closed`, because a recorded decline beats closure (gentle-pi#1175).
@@ -1048,7 +1093,7 @@ async function resolveReviewAssessmentPlan(
1048
1093
  nativeRisk,
1049
1094
  ...(input.escalate === undefined
1050
1095
  ? {}
1051
- : { agentEscalation: { item: input.escalate.item, label: HIGH_RISK_ITEMS[input.escalate.item], reason: input.escalate.reason, applied: risk !== nativeRisk } }),
1096
+ : { agentEscalation: { item: input.escalate.item, label: HIGH_RISK_ITEMS[input.escalate.item], reason: input.escalate.reason, applied: risk !== verificationRisk(nativeRisk, undefined) } }),
1052
1097
  // No path is known for an unassessable candidate: omit it rather than
1053
1098
  // emitting an empty string the native reason shape forbids.
1054
1099
  reasons: assessment?.reasons ?? (unassessableDetail === undefined ? [] : [{ code: unassessableCode, detail: unassessableDetail }]),
@@ -1119,14 +1164,15 @@ async function resolveRddModeStatus(
1119
1164
  // read status instead of flipping the prompt to "unknown" and back, which
1120
1165
  // would invalidate the provider's prefix cache for the whole conversation.
1121
1166
  // Only a status the native reader actually reports replaces a known one.
1122
- const read = await readRddModeStatusOnce(nativeReviewCli, cwd, signal);
1167
+ const outcome = await readRddModeStatusWithCause(nativeReviewCli, cwd, signal);
1168
+ const read = "status" in outcome ? outcome.status : undefined;
1123
1169
  const status = read ?? cached?.status;
1124
1170
  rddStatusMemo.set(cwd, { status, expiresAt: nowMs + RDD_STATUS_MEMO_TTL_MS });
1125
- if (read === undefined && !rddStatusUnavailableWarned) {
1171
+ if ("cause" in outcome && !rddStatusUnavailableWarned) {
1126
1172
  rddStatusUnavailableWarned = true;
1127
1173
  if (ctx?.hasUI) {
1128
1174
  ctx.ui.notify(
1129
- "Gentle AI: receipt-driven-development status is unavailable (native review CLI absent, timed out, or failed). The parent prompt keeps the last known status, or renders \"unknown\" when none was read; this notice will not repeat this session.",
1175
+ `Gentle AI: receipt-driven-development status is unavailable: ${outcome.cause}. The parent prompt keeps the last known status, or renders "unknown" when none was read; this notice will not repeat this session.`,
1130
1176
  "warning",
1131
1177
  );
1132
1178
  }
@@ -1139,6 +1185,46 @@ function isRddStatusLineOff(line: string | undefined): boolean {
1139
1185
  return line !== undefined && line.startsWith("Receipt-driven development: off ");
1140
1186
  }
1141
1187
 
1188
+ /**
1189
+ * gentle-shell#2195: the rendered line sits in the cached system-prompt
1190
+ * prefix, so a session keeps the first line it rendered and re-renders only
1191
+ * when the off state changes, because that also adds or drops the review
1192
+ * execution contract (gentle-shell#1494). An unknown-to-on flip or a
1193
+ * deciding-source change keeps the prefix; review tools still read the live
1194
+ * mode whenever they run, so the pinned text never grants or blocks anything.
1195
+ */
1196
+ function pinRddStatusLine(pinned: string | undefined, next: string): string {
1197
+ return pinned !== undefined && isRddStatusLineOff(pinned) === isRddStatusLineOff(next) ? pinned : next;
1198
+ }
1199
+
1200
+ /**
1201
+ * gentle-pi S11 T1: while the pinned line reads off, no review can start and
1202
+ * the orchestrator only calls ASSESS, so the primary request swaps the review
1203
+ * family's long guidance for this lean loadout: one short gentle_review
1204
+ * snippet, guideline, and description, with the capture, capture-group, and
1205
+ * scope declarations hidden (they stay registered and active). It follows the
1206
+ * pinned line, so it changes only when that line's off state flips, together
1207
+ * with the review execution contract; on and unknown keep the registered text.
1208
+ */
1209
+ const RDD_OFF_REVIEW_SNIPPET = "Assess an authorized change and return its verification plan (read-only)";
1210
+ const RDD_OFF_REVIEW_GUIDELINES: readonly string[] = [
1211
+ 'Receipt-driven development is off, so use gentle_review only for {"operation":"assess"} (read-only): after an authorized implementation, delegated or inline, call it over the diff and follow the returned plan; pass input {"escalate":{"item":1-6,"reason":"<one line>"}} only when the change touches a Task Size high-risk item, or {"baseRef":"<ref>","committedOnly":true} to assess a committed range.',
1212
+ "When ASSESS returns reviewDue or a native continuation, relay it unchanged and follow it; tests, builds, and functional checks still run when applicable, and no assessment authorizes delivery.",
1213
+ ];
1214
+ const RDD_OFF_REVIEW_DESCRIPTION = "Assess the current changes, or a committed range, and return the verification plan. Read-only; review outcomes never authorize delivery: commit, push, pull-request, and release commands follow ordinary repository policy.";
1215
+ const RDD_OFF_HIDDEN_REVIEW_TOOLS: readonly string[] = ["gentle_review_scope", "gentle_review_capture_group", "gentle_review_capture"];
1216
+
1217
+ interface ReviewToolPromptOptions {
1218
+ toolSnippets?: Record<string, string>;
1219
+ toolGuidelines?: Record<string, string[]>;
1220
+ }
1221
+
1222
+ /** Replaces gentle_review's snippet and guidelines in this run's prompt options only. */
1223
+ function applyRddOffReviewPrompt(options: ReviewToolPromptOptions | null | undefined): void {
1224
+ if (options?.toolSnippets?.gentle_review !== undefined) options.toolSnippets.gentle_review = RDD_OFF_REVIEW_SNIPPET;
1225
+ if (options?.toolGuidelines?.gentle_review !== undefined) options.toolGuidelines.gentle_review = [...RDD_OFF_REVIEW_GUIDELINES];
1226
+ }
1227
+
1142
1228
  /** Resolves and renders the RDD status line for a production call site in one call. */
1143
1229
  async function resolveRddStatusLine(
1144
1230
  nativeReviewCli: Pick<NativeReviewCli, "reviewMode"> | null | undefined,
@@ -1205,13 +1291,13 @@ function renderOrchestratorPrompt(
1205
1291
  // pinned at an 8192-byte budget (tests/orchestrator-budget.test.ts).
1206
1292
  const PROVIDER_CONTRACT_MIRROR_ROOT = join(PACKAGE_ROOT, "contracts", "review-provider-contract-mirror");
1207
1293
  const PROVIDER_CONTRACT_LOCK_FILE = "provider-contract.lock.json";
1208
- const PI_ORCHESTRATION_RUNTIME = "pi";
1294
+ const PI_MIRROR_RUNTIME = "pi";
1209
1295
 
1210
1296
  let reviewContractPromptFragmentCache: string | null | undefined;
1211
1297
  let reviewContractPromptMissingWarned = false;
1212
1298
 
1213
- // Verifies the mirrored orchestration/pi.md bytes against the lock's digest before injection (gentle-ai R1/R3).
1214
- function readMirroredReviewContractFragment(mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT): string | null {
1299
+ // Reads one mirrored bundle file after verifying its bytes against the lock's digest (gentle-ai R1/R3).
1300
+ function readVerifiedMirrorFile(mirrorRoot: string, bundlePath: string): { contractSemver: string; text: string } | null {
1215
1301
  try {
1216
1302
  const lockPath = join(mirrorRoot, PROVIDER_CONTRACT_LOCK_FILE);
1217
1303
  const lock = JSON.parse(readFileSync(lockPath, "utf8")) as {
@@ -1219,20 +1305,71 @@ function readMirroredReviewContractFragment(mirrorRoot: string = PROVIDER_CONTRA
1219
1305
  entries?: Record<string, unknown>;
1220
1306
  };
1221
1307
  if (typeof lock.contract_semver !== "string" || lock.contract_semver === "") return null;
1222
- const expectedSha256 = lock.entries?.[`orchestration/${PI_ORCHESTRATION_RUNTIME}.md`];
1308
+ const expectedSha256 = lock.entries?.[bundlePath];
1223
1309
  if (typeof expectedSha256 !== "string" || !/^[0-9a-f]{64}$/.test(expectedSha256)) return null;
1224
- const contractPath = join(mirrorRoot, `v${lock.contract_semver}`, "bundle", "orchestration", `${PI_ORCHESTRATION_RUNTIME}.md`);
1225
- const rawBytes = readFileSync(contractPath);
1310
+ const filePath = join(mirrorRoot, `v${lock.contract_semver}`, "bundle", ...bundlePath.split("/"));
1311
+ const rawBytes = readFileSync(filePath);
1226
1312
  const actualSha256 = createHash("sha256").update(rawBytes).digest("hex");
1227
1313
  if (!timingSafeEqual(Buffer.from(expectedSha256, "hex"), Buffer.from(actualSha256, "hex"))) return null;
1228
- const text = rawBytes.toString("utf8").trim();
1229
- if (text.length === 0) return null;
1230
- return `## Gentle AI review execution contract (mirrored provider bundle ${lock.contract_semver})\n\n${text}`;
1314
+ return { contractSemver: lock.contract_semver, text: rawBytes.toString("utf8") };
1231
1315
  } catch {
1232
1316
  return null;
1233
1317
  }
1234
1318
  }
1235
1319
 
1320
+ // Verifies the mirrored orchestration/pi.md bytes against the lock's digest before injection (gentle-ai R1/R3).
1321
+ function readMirroredReviewContractFragment(mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT): string | null {
1322
+ const file = readVerifiedMirrorFile(mirrorRoot, `orchestration/${PI_MIRROR_RUNTIME}.md`);
1323
+ if (file === null) return null;
1324
+ const text = file.text.trim();
1325
+ if (text.length === 0) return null;
1326
+ return `## Gentle AI review execution contract (mirrored provider bundle ${file.contractSemver})\n\n${text}`;
1327
+ }
1328
+
1329
+ // gentle-ai R3: the harness ODD steps 1-7, the phase-reporting line, and the
1330
+ // resume line come from the mirrored bundle's routing/pi.md (contract >= 1.4.0),
1331
+ // the ODD routing block Gentle AI renders for Pi, instead of a hand-copied
1332
+ // literal. The bundle's blank lines between them are not carried into the
1333
+ // prompt. Like assets/orchestrator.md, this is package-shipped prompt source:
1334
+ // a missing, tampered, or reshaped file fails closed instead of rendering a
1335
+ // harness without its workflow.
1336
+ const ODD_PROTOCOL_HEADING = "### ODD protocol (MANDATORY, in this order, on every request)";
1337
+ const ODD_PROTOCOL_STEP_COUNT = 7;
1338
+
1339
+ let oddProtocolStepsCache: string | undefined;
1340
+
1341
+ function extractOddProtocolLines(routing: string): string {
1342
+ const lines = routing.split("\n");
1343
+ const start = lines.indexOf(ODD_PROTOCOL_HEADING);
1344
+ const section: string[] = [];
1345
+ for (const line of start === -1 ? [] : lines.slice(start + 1)) {
1346
+ if (line.startsWith("#")) break;
1347
+ if (line !== "") section.push(line);
1348
+ }
1349
+ const shapeHolds =
1350
+ section.length === ODD_PROTOCOL_STEP_COUNT + 2 &&
1351
+ section.slice(0, ODD_PROTOCOL_STEP_COUNT).every((line, index) => line.startsWith(`${index + 1}. **`)) &&
1352
+ section[ODD_PROTOCOL_STEP_COUNT]!.startsWith("Phase reporting: ") &&
1353
+ section[ODD_PROTOCOL_STEP_COUNT + 1]!.startsWith("Resume an interrupted feature ");
1354
+ if (!shapeHolds) {
1355
+ throw new Error(`The mirrored provider bundle's routing/${PI_MIRROR_RUNTIME}.md ODD protocol section has an unexpected shape: expected ${JSON.stringify(ODD_PROTOCOL_HEADING)} followed by steps 1-${ODD_PROTOCOL_STEP_COUNT}, phase reporting, and resume.`);
1356
+ }
1357
+ return section.join("\n");
1358
+ }
1359
+
1360
+ function readMirroredOddProtocolSteps(mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT): string {
1361
+ const file = readVerifiedMirrorFile(mirrorRoot, `routing/${PI_MIRROR_RUNTIME}.md`);
1362
+ if (file === null) {
1363
+ throw new Error(`Gentle Shell cannot render its ODD steps: the mirrored provider bundle's routing/${PI_MIRROR_RUNTIME}.md is missing, unreadable, or fails digest verification. Reinstall gentle-pi.`);
1364
+ }
1365
+ return extractOddProtocolLines(file.text);
1366
+ }
1367
+
1368
+ function oddProtocolSteps(): string {
1369
+ oddProtocolStepsCache ??= readMirroredOddProtocolSteps();
1370
+ return oddProtocolStepsCache;
1371
+ }
1372
+
1236
1373
  function loadReviewContractPromptFragment(
1237
1374
  ctx: Pick<ExtensionContext, "hasUI" | "ui">,
1238
1375
  mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT,
@@ -1324,27 +1461,27 @@ For background work, launch the bounded action and end the turn when no independ
1324
1461
 
1325
1462
  Default workflow: Organic Driven Development (MANDATORY)
1326
1463
  Organic Driven Development (ODD) is the predefined workflow of this orchestrator. Every request enters it, without the user asking for a workflow, a plan, or task tracking. Never describe this workflow only when asked about it: run it. Run these steps in this order on every request:
1327
- 1. **Authorize.** Investigation, explanation, review, comparison, and proposal-only requests stay read-only: no writer, apply, or implementation artifacts. Ambiguous or conditional change intent (unclear whether a change is authorized at all) gets one clarification; stop and wait. A user saying they may stop you or resume later asks for notes and separate commits, not a stop.
1328
- 2. **Explore.** Explore existing code and requirements first, proportionately to the request, before proposing or writing anything. Do not delegate exploration of files you will read anyway to work inline; explore only for a map you need to decide or route.
1329
- 3. **Resolve uncertainty.** Recommend optional research only for a named uncertainty; ask one focused user question only for a real unresolved product decision, then stop and wait; use at most one scoped read-only assumption challenge for a high-consequence unproven premise.
1330
- 4. **Classify.** Size the task by the orchestrator's Task Size section: small when understood, risk is contained, and the work could be resumed from the original request and \`git diff\` alone; large only when that resume test fails. Never classify by counting files, commands, tests, fixes, or a requested todo list. Small work stays inline and creates no durable task artifacts.
1331
- 5. **Track before the first write.** For large authorized implementation, create \`odd/tasks/<feature-name>.md\` and its Engram mirror \`odd/<feature-name>/tasks\` automatically, then create or rebuild the visible \`todo\` list from the reconciled feature tasks, all before the first source write and without asking permission for tasks or storage. Tell the user in one line which feature document was created and how many tasks it holds. The document is the specification subagents read, in this order: a two- or three-line header; \`## Specs\` with numbered \`S#\` that quote the user's exact strings, error messages, and examples verbatim, never summarized and never adding unrequested requirements; \`## Tasks\` with one line per task (ID, linked \`S#\`, route, commit); \`## Log\` last, where \`L1\` is the user's original request verbatim and later user corrections, evidence, and decisions are appended. A requirement change appends its verbatim Log entry, rewrites only the affected \`S#\`, and reopens only its task.
1332
- 6. **Implement task by task.** Hand off by reference, never by paraphrase: name the document, task, and specs (for example \`Spec: odd/tasks/<feature>.md, T2, S3-S4\`), tell workers to read until \`## Log\`, and ask which \`S#\` were covered. Without a feature document, include the user's request verbatim. Verify reads the whole document, runs the spec's examples the parent authorized, against isolated state when they mutate data, and returns a verdict per \`S#\`. When the user reports a failure, reproduce it before deciding it already works. Each test asserts every observable effect of the rule it covers (exit code, exact stdout and stderr, and that rejected input leaves stored data and counters unchanged), covers the cases the rule itself names (its examples, boundaries, and errors), and checks through the public interface, never internal storage. When you add or change a command, option, or message, update the help text and docs that describe it. Route each task through the orchestrator's Mechanisms, honoring its mandatory delegation triggers, with applicable test-first development and checks. These triggers are mandatory, not advisory: executing past a fired trigger inline is a routing defect even if the work succeeds. Check an item off only after its outcome and checks were observed; update the file, mirror, and visible \`todo\` projection after every task transition and material plan change. Every tracked task closes with at least one work-unit commit on the feature branch, branch first when on the default branch, with tests and docs alongside the behavior, using a Conventional Commit message; record the commit identity in the feature document as evidence. Work-unit commits on the feature branch are part of authorized large ODD implementation; push, pull request creation, and merge remain the user's decisions.
1333
- 7. **Close.** Report the verified outcome, every failed, skipped, or pending check, and the next step. Before writing \`Risk: none\`, check whether your diff changes code that existing behavior the request did not mention also uses (shared options, parsers, helpers); if it does, that is item 3. Never end with a tracked task pending unless you quote the user's explicit stop. An applicable quick check runs once; an unavailable verifier or subagent is reported as unavailable, never retried or escalated into extra ceremony. Partial, blocked, unavailable, or exhausted proof becomes one **Needs your decision** result naming the open blockers or missing proof, never more verification; that result is a valid stop, hedged wording is not. The native review candidate is a work-unit commit or a PR slice, never a TODO checkbox and never the accumulated feature branch; native review runs only under the user-owned RDD switch.
1334
- Phase reporting: the Gentle Shell prompt label is inferred automatically from the primary session's tool activity (reads show \`exploring\`, edits \`implementing\`, test runs \`checking\`, user questions \`deciding\`). When the \`gentle_odd_phase\` tool is available, use it to refine that label with phases tools cannot show (\`authorizing\`, \`researching\`, \`deciding\`, \`closing\`): call \`gentle_odd_phase\` only when the primary session's ODD phase actually changes, never per tool call or on a fixed cadence, and never from a subagent. It drives the Gentle Shell prompt label only.
1335
- Resume an interrupted feature with \`mem_context\`, then project- and feature-scoped \`mem_search\`, then \`mem_get_observation\` for the full document, then the task file itself; reconcile before continuing the next unfinished task. Detail for steps 3–7: \`orchestrator-delegation.md\` and \`orchestrator-memory.md\`.
1464
+ ${oddProtocolSteps()}
1336
1465
 
1337
1466
  Harness principles:
1338
1467
  - el Gentleman is not prompt engineering. It is runtime discipline around powerful agents.
1339
- - Organic Driven Development (ODD) is the predefined workflow for every request: authorize, explore, resolve uncertainty, classify, track large work before the first write, implement task by task with proportionate checks, close each tracked task with a work-unit commit, and close.
1340
1468
  - Clarify scope, constraints, acceptance criteria, and non-goals before implementation.
1341
1469
  - Use subagents when available for exploration, planning, implementation, and review, while keeping one parent session responsible for orchestration.
1342
- - Parallel writers only with disjoint Allowed edit surfaces (runtime-enforced) or isolated worktrees.
1343
- - For behavior changes with applicable runnable deterministic tests and a clear expected outcome, use test-first by default: observe RED, GREEN, then refactor with focused checks. Write one RED test per requested rule. For every existing command or option the change touches, add one test proving its previous behavior still holds; add no other cases. An existing behavior counts as touched when it shares the code you changed (options, parsers, helpers, validation). Test presence alone does not establish applicability; no chat or TUI toggle activates it. For passive documentation, non-testable changes, an unavailable runner, or no meaningful RED, explain why and run proportionate ordinary functional or structural verification. Never invent lifecycle evidence or skip checks. Follow orchestrator-delegation.md for ODD forwarding and evidence.
1470
+ - For behavior changes with applicable runnable deterministic tests and a clear expected outcome, use test-first by default: observe RED, GREEN, then refactor with focused checks. Write one RED test per requested rule. For every existing command or option the change touches, add one test proving its previous behavior still holds; add no other cases. An existing behavior counts as touched when it shares the code you changed (options, parsers, helpers, validation). Before finishing, run a differential check: for each existing command that shares the code you changed, run it with the new or changed input (for example a new option passed to a sibling subcommand) against the previous code (a copy of the base commit in a \`mktemp -d\` scratch directory, never committed as a test; committed tests never read git history or a fixed commit) and against your change, and compare stdout, stderr, and exit code; any difference nobody asked for is a regression to fix and pin with a test. Test presence alone does not establish applicability; no chat or TUI toggle activates it. For passive documentation, non-testable changes, an unavailable runner, or no meaningful RED, explain why and run proportionate ordinary functional or structural verification. Never invent lifecycle evidence or skip checks. Follow orchestrator-delegation.md for ODD forwarding and evidence.
1344
1471
  - Protect the human reviewer: avoid oversized changes, surface review workload risk, and ask before turning one task into a large multi-area change.
1345
1472
  - Never claim persistent memory is available because of this package. Memory is provided by separate packages or MCP tools when installed and callable.
1346
1473
 
1347
- ${getOrchestratorPrompt(cwd, activeTools, rddStatusLine, hostMode)}`;
1474
+ ${getOrchestratorPrompt(cwd, activeTools, rddStatusLine, hostMode)}${headlessDeliveryNote(hostMode)}`;
1475
+ }
1476
+
1477
+ // Bench gate-p53-r1-BS40 x6 run 3: in a headless RPC session the ask-on-risk
1478
+ // delivery question ran before any code was written, ask_user_question was
1479
+ // unavailable, and the orchestrator printed the menu and stopped. Hosts where
1480
+ // no human can answer (the same rule ask_user_question applies) resolve that
1481
+ // question to the recommended chain. Interactive sessions render nothing here.
1482
+ function headlessDeliveryNote(hostMode: string | undefined): string {
1483
+ if (hostMode === undefined || isInteractiveMode(hostMode)) return "";
1484
+ return "\n\nNo human can answer questions in this session. For the ask-on-risk delivery question, do not ask: use `feature-branch-chain`, record that choice in the feature document's `## Log`, and continue. Consent, maintenance, and opaque-token prompts keep their own rules.";
1348
1485
  }
1349
1486
 
1350
1487
  // Matches `git [global-flags] push` — tolerates flags like -C /repo or --work-tree=/tmp
@@ -1859,8 +1996,15 @@ const YOLO_SEGMENT_END_RE = /&&|\|\||;|\||\n|\r/;
1859
1996
  // most a `-C <path>`. Other global options (`-c key=value`, `--exec-path`,
1860
1997
  // `--git-dir`) can run or redirect code, so they keep the confirmation.
1861
1998
  const YOLO_GIT_PREFIX_RE = /^\s*(?:[A-Za-z_][A-Za-z0-9_]*=[^\s'"`$]*\s+|env\s+(?:-i\s+)?|command\s+)*git(?:\s+-C\s+[^\s'"`$;&|]+)?\s+$/;
1862
- const YOLO_PUSH_SEGMENT_RE = /^push(?:\s+(?:-u|--set-upstream|-q|--quiet|-v|--verbose|--tags|--follow-tags|--no-verify|--porcelain|[A-Za-z0-9_][A-Za-z0-9_./-]*(?::[A-Za-z0-9_][A-Za-z0-9_./-]*)?))*\s*$/;
1863
- const YOLO_REBASE_SEGMENT_RE = /^rebase(?:\s+(?:--onto\s+[A-Za-z0-9_][A-Za-z0-9_./~^-]*|--continue|--abort|--skip|--autostash|--no-autostash|[A-Za-z0-9_][A-Za-z0-9_./~^-]*))*\s*$/;
1999
+ // Output redirections that write nothing: to stdout/stderr or /dev/null.
2000
+ const YOLO_REDIRECT_SRC = String.raw`[12]?>&[12]|(?:[12]?|&)>>?\s*/dev/null(?![^\s;&|])`;
2001
+ const YOLO_PUSH_SEGMENT_RE = new RegExp(String.raw`^push(?:\s+(?:${YOLO_REDIRECT_SRC}|-u|--set-upstream|-q|--quiet|-v|--verbose|--tags|--follow-tags|--no-verify|--porcelain|[A-Za-z0-9_][A-Za-z0-9_./-]*(?::[A-Za-z0-9_][A-Za-z0-9_./-]*)?))*\s*$`);
2002
+ const YOLO_REBASE_SEGMENT_RE = new RegExp(String.raw`^rebase(?:\s+(?:${YOLO_REDIRECT_SRC}|--onto\s+[A-Za-z0-9_][A-Za-z0-9_./~^-]*|--continue|--abort|--skip|--autostash|--no-autostash|[A-Za-z0-9_][A-Za-z0-9_./~^-]*))*\s*$`);
2003
+ // A pipe out of a waived segment may only feed read-only filters with plain or
2004
+ // literally quoted arguments and end at a sequential separator. Git output
2005
+ // carries remote-controlled text, so `| sh`, `| tee`, `| xargs` and a trailing
2006
+ // background `&` keep the confirmation.
2007
+ const YOLO_FILTER_PIPELINE_RE = new RegExp(String.raw`^(?:\|(?!\|)\s*(?:tail|head|cat|grep|wc)(?:\s+(?:${YOLO_REDIRECT_SRC}|[A-Za-z0-9_.,:=+/%@-]+|'[^'\n]*'|"[^"$\x60\\\n]*"))*\s*)*(?=$|&&|\|\||;|\n|\r)`);
1864
2008
 
1865
2009
  function yoloGuardedSegment(command: string, triggerIndex: number): string {
1866
2010
  const rest = command.slice(triggerIndex);
@@ -1875,20 +2019,29 @@ function yoloSegmentPrefix(command: string, triggerIndex: number): string {
1875
2019
  return last === undefined ? head : head.slice(last.index + last[0].length);
1876
2020
  }
1877
2021
 
1878
- /** Every guarded match and every recognized Git side effect must be a reversible push or non-interactive rebase. */
1879
- function isReversibleYoloCommand(command: string, evaluation: GuardEvaluation): boolean {
1880
- if (evaluation.action !== "confirm" || evaluation.dataLoss || evaluation.matches.length === 0) return false;
2022
+ /**
2023
+ * Every guarded match and every recognized Git side effect must be a reversible
2024
+ * push or non-interactive rebase; the only data loss allowed is recursive
2025
+ * removal of the session's own temp scratch (lib/yolo-temp-scratch.ts).
2026
+ */
2027
+ function isReversibleYoloCommand(command: string, evaluation: GuardEvaluation, scratchFs?: TempScratchFs): boolean {
2028
+ if (evaluation.action !== "confirm") return false;
2029
+ const effects = recognizeDestructiveCommands(command);
2030
+ const scratch = effects.map((effect) => effect.removalTargets !== undefined && isTempScratchRemoval(effect, scratchFs ??= defaultTempScratchFs()));
2031
+ if (evaluation.matches.length === 0 && !scratch.includes(true)) return false;
1881
2032
  for (const match of evaluation.matches) {
1882
2033
  if (!YOLO_WAIVABLE_KEYS.has(match.key)) return false;
1883
2034
  if (!YOLO_GIT_PREFIX_RE.test(yoloSegmentPrefix(command, match.triggerIndex))) return false;
1884
2035
  const segment = yoloGuardedSegment(command, match.triggerIndex);
1885
2036
  if (!(match.key === "gitPush" ? YOLO_PUSH_SEGMENT_RE : YOLO_REBASE_SEGMENT_RE).test(segment)) return false;
2037
+ if (!YOLO_FILTER_PIPELINE_RE.test(command.slice(match.triggerIndex + segment.length))) return false;
1886
2038
  }
1887
2039
  // The data-loss recognizer leaves non-hard Git effects (reset, stash drop,
1888
2040
  // restore, checkout --) to the primary policy; under YOLO only a rebase that
1889
2041
  // a guarded match already vetted may remain.
1890
2042
  const rebaseTriggers = new Set(evaluation.matches.filter((match) => match.key === "gitRebase").map((match) => match.triggerIndex));
1891
- for (const effect of recognizeDestructiveCommands(command)) {
2043
+ for (const [index, effect] of effects.entries()) {
2044
+ if (scratch[index]) continue;
1892
2045
  if (effect.hardDeny || effect.kind !== "git") return false;
1893
2046
  const rebase = /^git(?:\s+(?:-C|-c|--git-dir|--work-tree|--namespace)\s+\S+|\s+-\S+)*\s+(rebase)\b/.exec(command.slice(effect.triggerIndex));
1894
2047
  if (rebase === null || !rebaseTriggers.has(effect.triggerIndex + rebase.index + rebase[0].lastIndexOf("rebase"))) return false;
@@ -5140,7 +5293,7 @@ const REVIEW_CONTROLLER_PARAMETER_FIELDS = {
5140
5293
  },
5141
5294
  input: {
5142
5295
  type: "string",
5143
- description: "A JSON object or serialized object string for START/ASSESS only; every other operation requires a serialized object string. New native ordinary START uses {\"mode\":\"ordinary\"}, plus the agent lens selection {\"lenses\":[\"risk\",...],\"lensesReason\":\"<what you touched and how>\"} (both or neither, 1-4 of risk, resilience, readability, reliability; never with focus; providers that predate it keep the tier default); answer-consent uses exactly {\"consentBinding\":\"<opaque id>\",\"answer\":\"granted|declined\"}. Ordinary provider capture belongs only to gentle_review_capture. An explicit baseRef requires committedOnly: true and requests a committed range, while repository-local policyPath remains optional. baseRef must be HEAD, a full 40- or 64-character commit id, or a ref name; abbreviated commit ids are rejected as base-ref-unresolvable. ASSESS accepts an optional object with baseRef, committedOnly, writerModelId, writerEffort, and nativeReviewOutcome (gentle-pi#662/#668/#1175). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); writerModelId and writerEffort are only a fallback when no runtime evidence exists (caller), and with neither the profile fails closed to small (fallback). A missing model, a mini model token (gemini is not mini), or low effort keeps the conservative small-model bias. ASSESS derives `closed` only from the native candidate.consumed fact for this exact candidate, which native records only when the approved review for that candidate is acknowledged; a caller-supplied closed is not authority and, without that fact, resolves to unknown. Every outcome yields the exact risk-gated plan ASSESS returns when RDD is off: a closed review never removes the separate verifier the tier requires (verify-always-rdd-high S1), unknown is never treated as closed, and a decline is candidate-scoped and never lowers the bar below the RDD-off path. A declined or unavailable outcome, stated explicitly or recorded by this process for this exact candidate (never a different one, and never from repository state alone), wins over closure. The returned outcome_source (explicit|derived|unknown) says which of these produced the value. Legacy controller input remains separate.",
5296
+ description: "A JSON object or serialized object string for START/ASSESS only; every other operation requires a serialized object string. New native ordinary START uses {\"mode\":\"ordinary\"}, plus the agent lens selection {\"lenses\":[\"risk\",...],\"lensesReason\":\"<what you touched and how>\"} (both or neither, 1-4 of risk, resilience, readability, reliability; never with focus; providers that predate it keep the tier default); answer-consent uses exactly {\"consentBinding\":\"<opaque id>\",\"answer\":\"granted|declined\"}. Ordinary provider capture belongs only to gentle_review_capture. An explicit baseRef requires committedOnly: true and requests a committed range, while repository-local policyPath remains optional. baseRef must be HEAD, a full 40- or 64-character commit id, or a ref name; abbreviated commit ids are rejected as base-ref-unresolvable. ASSESS accepts an optional object with baseRef, committedOnly, writerModelId, writerEffort, and nativeReviewOutcome (gentle-pi#662/#668/#1175). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); writerModelId and writerEffort are only a fallback when no runtime evidence exists (caller), and with neither the profile fails closed to small (fallback). The writer profile is reported but never adds a verifier: the plan adds the independent verifier only when escalate names a Task Size high-risk item. ASSESS derives `closed` only from the native candidate.consumed fact for this exact candidate, which native records only when the approved review for that candidate is acknowledged; a caller-supplied closed is not authority and, without that fact, resolves to unknown. Every outcome yields the exact risk-gated plan ASSESS returns when RDD is off: a closed review never removes the separate verifier the tier requires (verify-always-rdd-high S1), unknown is never treated as closed, and a decline is candidate-scoped and never lowers the bar below the RDD-off path. A declined or unavailable outcome, stated explicitly or recorded by this process for this exact candidate (never a different one, and never from repository state alone), wins over closure. The returned outcome_source (explicit|derived|unknown) says which of these produced the value. Legacy controller input remains separate.",
5144
5297
  },
5145
5298
  outputPath: { type: "string", description: "Retired with legacy bundle export; ignored. Export returns legacy-operation-retired." },
5146
5299
  inputPath: { type: "string", description: "Repository-local JSON input file for the separate legacy controller flow (alternative to input). Legacy bundle import is retired." },
@@ -6611,6 +6764,8 @@ const processRetainedNativeStatusSelections = new Map<PendingReviewConsentSessio
6611
6764
  // named-agent start increments the depth, a matching end decrements it,
6612
6765
  // and a fresh primary-loop start resets it to 0.
6613
6766
  const processAgentEndSubagentDepth = new Map<PendingReviewConsentSessionKey, number>();
6767
+ // gentle-shell#2195: the RDD status line each primary session last rendered into its system prompt.
6768
+ const pinnedRddStatusLines = new Map<PendingReviewConsentSessionKey, string>();
6614
6769
 
6615
6770
  // gentle-shell#1064 slice 1: the parent-session profile binding store lives in
6616
6771
  // lib/session-profile-binding.ts (in-process, keyed by parent session id). The
@@ -7990,6 +8145,20 @@ function isSelectedReviewCapture(value: SelectedReviewCapture | Record<string, u
7990
8145
 
7991
8146
  function captureGroupRejected(reason: string): Record<string, unknown> { return captureBindingRejected(reason, true); }
7992
8147
 
8148
+ function captureGroupStatusFailure(error: unknown): Record<string, unknown> {
8149
+ return {
8150
+ ...captureGroupRejected(error instanceof Error ? error.message : String(error)),
8151
+ outcome: "native-status-failed",
8152
+ // Only project the decoded native envelope, never arbitrary error fields.
8153
+ // Group mutation reporting belongs to its submitted-reviewer progress.
8154
+ ...(error instanceof NativeReviewIntegrationError ? {
8155
+ native_failure: error.failureEnvelope.raw,
8156
+ replayability: error.failureEnvelope.replayability,
8157
+ next_action: error.failureEnvelope.nextAction,
8158
+ } : {}),
8159
+ };
8160
+ }
8161
+
7993
8162
  function hasExactReviewCaptureSuffix(status: ReviewStatusV3, expected: readonly string[]): boolean {
7994
8163
  const current = status.nextTransition?.kind === "collect"
7995
8164
  ? (status.nextTransition.collect?.inputs ?? []).filter((input) => input.captureOperation === "review.capture-result").map(canonicalReviewCaptureBinding)
@@ -8296,7 +8465,7 @@ async function executeReviewCaptureGroupOperation(
8296
8465
  if (negotiated.transport !== undefined) return hostTransportUnavailable("gentle_review_capture_group", negotiated.transport);
8297
8466
  status = negotiated.status!;
8298
8467
  } catch (error) {
8299
- return { ...captureGroupRejected(error instanceof Error ? error.message : String(error)), outcome: "native-status-failed" };
8468
+ return captureGroupStatusFailure(error);
8300
8469
  }
8301
8470
  const group = selectExactReviewCaptureGroup(status, parameters.lineageId, canonicalBindings);
8302
8471
  if (!isSelectedReviewCaptureGroup(group)) return group;
@@ -8342,7 +8511,7 @@ async function executeReviewCaptureGroupOperation(
8342
8511
  if (!hasExactReviewCaptureSuffix(negotiated.status!, canonicalBindings.slice(index))) return { ...captureGroupAuthorityDrift(negotiated.status!), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
8343
8512
  current = selectExactReviewCapture(negotiated.status!, parameters.lineageId, canonicalBindings[index]!);
8344
8513
  } catch (error) {
8345
- return { ...captureGroupRejected(error instanceof Error ? error.message : String(error)), outcome: "native-status-failed", ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
8514
+ return { ...captureGroupStatusFailure(error), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
8346
8515
  }
8347
8516
  if (!isSelectedReviewCapture(current)) return { ...captureGroupRejected(String(current.reason ?? "current STATUS rejected a reviewer binding")), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
8348
8517
  try {
@@ -9477,6 +9646,7 @@ export const __testing = {
9477
9646
  renderOrchestratorPrompt,
9478
9647
  loadReviewContractPromptFragment,
9479
9648
  readMirroredReviewContractFragment,
9649
+ readMirroredOddProtocolSteps,
9480
9650
  loadBackgroundSubagentsPolicy,
9481
9651
  resolveBackgroundSubagentsPolicy,
9482
9652
  renderBackgroundSubagentsReport,
@@ -9486,6 +9656,7 @@ export const __testing = {
9486
9656
  readActiveToolNames,
9487
9657
  renderBackgroundSubagentsStatusLine,
9488
9658
  renderRddStatusLine,
9659
+ pinRddStatusLine,
9489
9660
  isValidRddModeStatus,
9490
9661
  resolveRddModeStatus,
9491
9662
  resolveRddStatusLine,
@@ -9607,11 +9778,17 @@ function createGentleAiExtensionForTesting(
9607
9778
  reviewSidebar.reset(ctx);
9608
9779
  statusTiming.reset(ctx);
9609
9780
  });
9781
+ // Lean RDD-off review loadout (applyRddOffReviewPrompt): every
9782
+ // before_agent_start sets it from the pinned line before Pi prepares that
9783
+ // run's tool loadout, so gentle_review's prepareLoadout reads this run's value.
9784
+ let rddOffReviewLoadout = false;
9610
9785
  let reminderSessionActive = true;
9611
9786
  let reminderEpoch = 0;
9612
9787
  let reminderManager: ExtensionContext["sessionManager"] | undefined;
9613
9788
  let unbindPreparation: (() => void) | undefined;
9614
9789
  pi.on("session_shutdown", (event, context) => {
9790
+ pinnedRddStatusLines.delete(pendingReviewConsentSessionKey(context, pendingReviewConsentFallbackKey));
9791
+ rddOffReviewLoadout = false;
9615
9792
  auto.reset();
9616
9793
  yolo.reset(context);
9617
9794
  reviewSidebar.reset();
@@ -9859,10 +10036,20 @@ function createGentleAiExtensionForTesting(
9859
10036
  "For blocked-legacy or blocked-mixed, do not call START repeatedly. Explain invalidation, request explicit user authorization, then call RESET or RECOVER only after authorization. RESET and RECOVER_LOCK route to audited native `gentle-ai review reclaim`; only RESET carries the legacy repositoryId, commonDirHash, inventoryHash, and confirmation challenge. RECOVER routes to native `gentle-ai review recover` with exactly six inputs: predecessorLineage, expectedPredecessorRevision, successorLineage, disposition, actor, and reason. Never send RECOVER the reset challenge and never send it a maintainerAuthorization: Pi reads fresh native target status, pins the predecessor lineage, revision, provider-selected disposition, and target identity, derives the exact six-line native authorization binding, displays it for fresh UI approval, and re-reads status before mutating. Negotiated target status supplies the sole accepted recovery disposition, and a caller-supplied substitute is rejected. Treat a native-input-required envelope as a request for exact values, never as permission to invent them. After a committed native recovery record, INSPECT before any fresh ordinary START.",
9860
10037
  "A consent-required START may be resolved inside the eligible interactive Pi host. Its third UI action is host-owned: it runs this envelope's exact provider grant once and allows later fresh validated envelopes only for the same live SessionManager, nonempty session ID, and canonical Git common-directory identity, including sibling worktrees; an unrelated repository requires a new explicit human grant. Revoke removes the current repository grant, while nonreload replacement, quit, and process exit remove all session grants; reload preserves them. It grants no provider mode, verdict, acknowledgement, maintenance, delivery, or cross-repository authority. A package-owned child may ask its parent only with the canonical digest of its exact pending target; the parent binds that digest to the task repository and fails closed otherwise. If the tool returns an unresolved envelope, present the original two provider choices without changing machine tokens, commands, target IDs, or invocations; never add the host action to the decoded provider envelope. After one explicit relayed human answer, call answer-consent exactly once with only consentBinding and answer (`granted` or `declined`). Never create host permission from tool arguments, model prose, child/headless responses, or an uncertain native result. A reported lineage_created false or pre-authority validation error proves no lineage was created. After ambiguous START output, the controller calls target-scoped native status once and returns only its declared action. An ambiguous gentle_review_capture outcome independently reconciles once and never replays the capture.",
9861
10038
  "Use gentle_review only for native review authority operations; delivery commands follow ordinary repository policy.",
9862
- 'ASSESS (gentle-pi#662/#668) is read-only and needs no lineageId: after a delegated writer returns, call {"operation":"assess"} over its diff and follow the returned plan (writerSelfVerification, structuralReadbackOnly, independentVerifier, reason) instead of judging non-triviality from the task description. Pass input as JSON only to assess a committed range ({"baseRef":"<ref>","committedOnly":true}), to supply a fallback writer profile ({"writerModelId":"...", "writerEffort":"..."}), or to state that the native review was declined or unavailable for this candidate ({"nativeReviewOutcome":"declined|unavailable|unknown"}), or to raise a candidate you know is high risk ({"escalate":{"item":1-6,"reason":"<one line>"}}, item from the Task Size high-risk list; it raises passive or medium to high, keeps nativeRisk, returns agentEscalation, and can never lower a tier). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); caller writerModelId/writerEffort are only a fallback when no runtime evidence exists (caller), otherwise the profile is small (fallback). A missing model, a mini model token (gemini is not mini), or low effort keeps the conservative small-model bias. The plan follows the native risk tier whatever RDD reads and whatever the native review outcome: the native review is an additional outside view and never removes a verifier the tier requires (verify-always-rdd-high S1). ASSESS derives closed only from the native candidate.consumed fact for this exact candidate, written natively when the approved review for that candidate is acknowledged; a caller-supplied closed is not authority and, without that fact, resolves to unknown; the outcome only explains the plan. A decline or unavailable review recorded by this process is bound to that exact candidate\'s own target identity, never to a different candidate or to bare repository state, and wins over closure. The result\'s outcome_source (explicit|derived|unknown) and writerProfileSource (runtime|caller|fallback) state which evidence produced each value. When native reports them, ASSESS projects reviewDue, reviewDueReason, candidate.consumed, and the native continuation verbatim; older binaries omit them and nothing is invented. Relay that continuation unchanged; never rebuild it. A native code review is not a substitute for applicable functional checks: tests, builds, and functional verification such as browser checks for UI changes still run when applicable. A failed or unavailable native assessment reports risk "unassessable", verified exactly like "high". This never mutates review authority state.',
10039
+ 'ASSESS (gentle-pi#662/#668) is read-only and needs no lineageId: after a delegated writer returns, call {"operation":"assess"} over its diff and follow the returned plan (writerSelfVerification, structuralReadbackOnly, independentVerifier, reason) instead of judging non-triviality from the task description. Pass input as JSON only to assess a committed range ({"baseRef":"<ref>","committedOnly":true}), to supply a fallback writer profile ({"writerModelId":"...", "writerEffort":"..."}), or to state that the native review was declined or unavailable for this candidate ({"nativeReviewOutcome":"declined|unavailable|unknown"}), or to raise a candidate you know is high risk ({"escalate":{"item":1-6,"reason":"<one line>"}}, item from the Task Size high-risk list; it is the only way to get an independent verifier, keeps nativeRisk, returns agentEscalation, and can never lower a tier). The writer profile comes from the runtime-recorded model and effort of the pending mutations for this root (writerProfileSource runtime); caller writerModelId/writerEffort are only a fallback when no runtime evidence exists (caller), otherwise the profile is small (fallback). The writer profile is reported but never adds a verifier. The plan follows its risk tier whatever RDD reads and whatever the native review outcome: the native review is an additional outside view and never removes a verifier the tier requires (verify-always-rdd-high S1). ASSESS derives closed only from the native candidate.consumed fact for this exact candidate, written natively when the approved review for that candidate is acknowledged; a caller-supplied closed is not authority and, without that fact, resolves to unknown; the outcome only explains the plan. A decline or unavailable review recorded by this process is bound to that exact candidate\'s own target identity, never to a different candidate or to bare repository state, and wins over closure. The result\'s outcome_source (explicit|derived|unknown) and writerProfileSource (runtime|caller|fallback) state which evidence produced each value. When native reports them, ASSESS projects reviewDue, reviewDueReason, candidate.consumed, and the native continuation verbatim; older binaries omit them and nothing is invented. Relay that continuation unchanged; never rebuild it. A native code review is not a substitute for applicable functional checks: tests, builds, and functional verification such as browser checks for UI changes still run when applicable. A failed or unavailable native assessment (nativeRisk "unassessable"), or a native high, informs only: the plan adds the independent verifier only when escalate names a Task Size high-risk item. This never mutates review authority state.',
9863
10040
  ],
9864
10041
  parameters: REVIEW_CONTROLLER_PARAMETERS,
9865
10042
  executionMode: "sequential",
10043
+ // Hidden declarations stay active and callable; Pi leaves their
10044
+ // declarations and guidelines out of the request.
10045
+ prepareLoadout(loadout) {
10046
+ if (!rddOffReviewLoadout) return undefined;
10047
+ const declared = new Set(loadout.declared.map((tool) => tool.name));
10048
+ return {
10049
+ descriptions: { gentle_review: RDD_OFF_REVIEW_DESCRIPTION },
10050
+ hiddenDeclarations: RDD_OFF_HIDDEN_REVIEW_TOOLS.filter((name) => declared.has(name)),
10051
+ };
10052
+ },
9866
10053
  renderCall(args, theme, context) {
9867
10054
  return renderGentleAiLifecycleCall(
9868
10055
  reviewToolOperationPath(args),
@@ -10139,7 +10326,13 @@ function createGentleAiExtensionForTesting(
10139
10326
  if (inFlightModeRead !== undefined) await Promise.race([inFlightModeRead, rddAbortRejection(rddStatusSignal)]).catch(() => undefined);
10140
10327
  const rddStatusLine = !isPrimarySession
10141
10328
  ? undefined
10142
- : await resolveRddStatusLine(nativeReviewCli, ctx.cwd, rddStatusSignal, undefined, ctx);
10329
+ : pinRddStatusLine(
10330
+ pinnedRddStatusLines.get(subagentDepthKey),
10331
+ await resolveRddStatusLine(nativeReviewCli, ctx.cwd, rddStatusSignal, undefined, ctx),
10332
+ );
10333
+ if (rddStatusLine !== undefined) pinnedRddStatusLines.set(subagentDepthKey, rddStatusLine);
10334
+ rddOffReviewLoadout = isPrimarySession && isRddStatusLineOff(rddStatusLine);
10335
+ if (rddOffReviewLoadout) applyRddOffReviewPrompt(event.systemPromptOptions as ReviewToolPromptOptions | undefined);
10143
10336
  const gentlePrompt = rddStatusLine === undefined
10144
10337
  ? ""
10145
10338
  : `\n\n${buildGentlePrompt(
@@ -10222,7 +10415,7 @@ function createGentleAiExtensionForTesting(
10222
10415
  consumeReviewMutation(pi, ctx.sessionManager, root, mutation, "nudged", targetIdentity);
10223
10416
  });
10224
10417
 
10225
- pi.on("tool_result", async (event, ctx) => {
10418
+ const recordWriteResult = async (event: ToolResultEvent, ctx: ExtensionContext): Promise<void> => {
10226
10419
  if (!reminderSessionActive || (reminderManager && reminderManager !== ctx.sessionManager) || event.isError !== false || (event.toolName !== "write" && event.toolName !== "edit")) return;
10227
10420
  if (!isRecord(event.input) || typeof event.input.path !== "string" || !event.input.path.trim()) return;
10228
10421
  try {
@@ -10234,9 +10427,33 @@ function createGentleAiExtensionForTesting(
10234
10427
  if (root) recordReviewMutation(pi, ctx.sessionManager, root, { source: "direct", toolName: event.toolName, toolCallId: event.toolCallId, ...directWriterProfile(pi, ctx) });
10235
10428
  if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && prospectiveRoot && !resolveSessionWorktree(ctx.cwd, ctx.cwd)) await prepareBoundSessionRepository(ctx.sessionManager, ctx.sessionManager.getCwd?.() ?? ctx.cwd, ctx.signal);
10236
10429
  } catch { /* Preparation and receipt persistence cannot change a successful tool result. */ }
10430
+ };
10431
+ const contextBackstopSignal = createContextBackstopSignal(permissionEnvironment);
10432
+ pi.on("tool_result", async (event, ctx) => {
10433
+ // Harness roadmap R1: the primary session's measured context is checked
10434
+ // against the backstop on every tool result; the first result at or past
10435
+ // it carries one notice, after any other patch below.
10436
+ const sessionKey = pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey);
10437
+ const primary = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(sessionKey) ?? 0) === 0;
10438
+ let notice: string | undefined;
10439
+ try { notice = primary ? contextBackstopSignal(sessionKey, ctx.getContextUsage?.()) : undefined; } catch { notice = undefined; }
10440
+ const withNotice = (patch: ToolResultEventResult | undefined) => notice ? appendContextBackstopNotice(event, patch, notice) : patch;
10441
+ // Harness roadmap T4: a parent commit reports the branch's authored
10442
+ // changed lines so delivery slicing reads a number, not an estimate.
10443
+ if (SHELL_COMMAND_TOOLS.has(event.toolName)) {
10444
+ if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD === "1") return undefined;
10445
+ try { return withNotice(await commitSliceToolResult(event, ctx.cwd)); } catch { return withNotice(undefined); }
10446
+ }
10447
+ await recordWriteResult(event, ctx);
10448
+ return withNotice(undefined);
10237
10449
  });
10238
10450
 
10239
10451
  pi.on("tool_call", async (event, ctx) => {
10452
+ // Native review tools stalled the session when a codemode script called them
10453
+ // (a nested call, which carries parentToolCallId); direct calls return at once.
10454
+ if (event.toolName.startsWith("gentle_review") && typeof (event as { parentToolCallId?: unknown }).parentToolCallId === "string") {
10455
+ return { block: true, reason: `${event.toolName} runs only as a direct tool call: call it directly, never from codemode or another tool.` };
10456
+ }
10240
10457
  const primaryToolCall = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)) ?? 0) === 0;
10241
10458
  const yoloActive = primaryToolCall && await yolo.active(ctx);
10242
10459
  const sensitivePathDenied = evaluateSensitivePathTool(
@@ -10244,11 +10461,22 @@ function createGentleAiExtensionForTesting(
10244
10461
  event.input,
10245
10462
  );
10246
10463
  if (sensitivePathDenied) return sensitivePathDenied;
10464
+ const deliveredTestDenied = evaluateDeliveredTestTool(event.toolName, event.input, ctx.cwd);
10465
+ if (deliveredTestDenied) return deliveredTestDenied;
10247
10466
  if (event.toolName === "subagent_run") {
10248
10467
  const judgmentDayFixDenied = rejectInvalidJudgmentDayFixDispatch(event.input);
10249
10468
  if (judgmentDayFixDenied) return judgmentDayFixDenied;
10250
10469
  const writerScopeDenied = rejectUnscopedBoundedWriterDispatch(event.input);
10251
10470
  if (writerScopeDenied) return writerScopeDenied;
10471
+ if (primaryToolCall) {
10472
+ const launch = event.input as Record<string, unknown>;
10473
+ const selected = [launch.workspace_root, launch.repository_root].find((value): value is string => typeof value === "string" && value.trim() !== "");
10474
+ const root = selected ? resolve(ctx.cwd, selected) : ctx.cwd;
10475
+ const undocumentedWriter = rejectUndocumentedWriterLaunch(event.input, { root, contextUsage: ctx.getContextUsage?.(), env: permissionEnvironment });
10476
+ if (undocumentedWriter) return undocumentedWriter;
10477
+ const unriskedVerifier = rejectUnriskedVerifierLaunch(event.input, { root, contextUsage: ctx.getContextUsage?.(), env: permissionEnvironment });
10478
+ if (unriskedVerifier) return unriskedVerifier;
10479
+ }
10252
10480
  try {
10253
10481
  injectReviewCandidateView(event.input, candidateViews);
10254
10482
  return undefined;
@@ -10402,6 +10630,10 @@ function createGentleAiExtensionForTesting(
10402
10630
  if (modelConfig.status === "invalid") {
10403
10631
  lines.push(`remedy: fix or remove ${modelConfig.path}`);
10404
10632
  }
10633
+ lines.push(...herdrSetupHint(process.env, {
10634
+ interactive: ctx.mode === "tui" && ctx.hasUI,
10635
+ isSocket: (path) => { try { return statSync(path).isSocket(); } catch { return false; } },
10636
+ }));
10405
10637
  ctx.ui.notify(
10406
10638
  lines.join("\n"),
10407
10639
  lines.some((line) => line.startsWith("fail:")) || assetLines.some((line) => line.startsWith("warn:")) ? "warning" : "info",