gentle-pi 4.1.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 (249) hide show
  1. package/README.md +352 -128
  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/engram-guard +1 -1
  12. package/bin/gentle-shell.mjs +188 -35
  13. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  14. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  15. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  16. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  17. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  19. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  20. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  21. package/docs/assets/diagrams/measured-routing.svg +49 -0
  22. package/docs/assets/diagrams/native-review.svg +52 -12
  23. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  24. package/docs/assets/diagrams/permission-modes.svg +51 -0
  25. package/docs/assets/features/audio-notifications.png +0 -0
  26. package/docs/assets/features/command-palette.png +0 -0
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +40 -8
  30. package/docs/delegated-verification.md +10 -8
  31. package/docs/gentle-agents-activity.md +11 -5
  32. package/docs/gentle-shell-uninstall.md +8 -1
  33. package/docs/gentle-shell.md +20 -12
  34. package/docs/install-wizard.md +17 -10
  35. package/docs/permission-modes.md +51 -6
  36. package/docs/readme-reference.md +155 -31
  37. package/docs/session-profile-format.md +39 -0
  38. package/docs/yolo-mode.md +21 -7
  39. package/extensions/ask-user-choice.ts +4 -0
  40. package/extensions/ask-user-question.ts +6 -1
  41. package/extensions/codegraph-tools.ts +68 -6
  42. package/extensions/gentle-agents.ts +146 -28
  43. package/extensions/gentle-ai.ts +380 -68
  44. package/extensions/gentle-herdr-activity.ts +2 -3
  45. package/extensions/gentle-shell.ts +53 -43
  46. package/extensions/gentle-todo.ts +62 -18
  47. package/extensions/quiet-tools.ts +1 -1
  48. package/extensions/session-scratch.ts +365 -0
  49. package/extensions/startup-banner.ts +2 -3
  50. package/lib/agent-home.ts +8 -3
  51. package/lib/agents-protocol.ts +3 -0
  52. package/lib/agents-runner.ts +55 -6
  53. package/lib/agents-view.ts +3 -1
  54. package/lib/auto-permission-answers.ts +133 -0
  55. package/lib/auto-permission-classifier.ts +215 -36
  56. package/lib/auto-permission-evidence.ts +88 -0
  57. package/lib/auto-permission-memory.ts +69 -0
  58. package/lib/auto-permission-runtime.ts +251 -35
  59. package/lib/auto-permission-tool-context.ts +61 -0
  60. package/lib/bounded-writer-admission.ts +51 -0
  61. package/lib/command-palette-catalog.ts +0 -4
  62. package/lib/commit-slice-signal.ts +100 -0
  63. package/lib/context-backstop.ts +84 -0
  64. package/lib/delivered-test-lint.ts +80 -0
  65. package/lib/destructive-command-guard.ts +215 -28
  66. package/lib/gauge-theme.ts +146 -0
  67. package/lib/gentle-ai-binary.ts +28 -7
  68. package/lib/gentle-shell-bundled.ts +62 -8
  69. package/lib/gentle-shell-import-pi.ts +233 -0
  70. package/lib/gentle-shell-launcher.ts +146 -12
  71. package/lib/gentle-shell-uninstall.ts +18 -1
  72. package/lib/herdr-activity.ts +24 -0
  73. package/lib/model-routing-authority.ts +6 -1
  74. package/lib/native-review-cli.ts +9 -1
  75. package/lib/orchestrator-discovery.ts +5 -1
  76. package/lib/orchestrator-presence.ts +9 -0
  77. package/lib/orchestrator-scope.ts +21 -3
  78. package/lib/provider-contract-bundle.ts +99 -53
  79. package/lib/review-candidate-view-owner.ts +29 -19
  80. package/lib/review-candidate-view.ts +36 -2
  81. package/lib/review-risk-assessment.ts +43 -38
  82. package/lib/review-session-standing-permission.ts +3 -1
  83. package/lib/selection-engine.ts +4 -1
  84. package/lib/session-profile-freeze.ts +40 -16
  85. package/lib/session-profile-persistence.ts +187 -0
  86. package/lib/session-scratch.ts +474 -0
  87. package/lib/shell-bar.ts +25 -25
  88. package/lib/shell-changes-view.ts +80 -13
  89. package/lib/shell-changes.ts +2 -0
  90. package/lib/shell-gauge.ts +49 -6
  91. package/lib/shell-prompt.ts +6 -4
  92. package/lib/shell-todo.ts +39 -8
  93. package/lib/shell-usage-view.ts +5 -1
  94. package/lib/shell-usage.ts +7 -7
  95. package/lib/verifier-admission.ts +29 -0
  96. package/lib/vim-editor-adapter.ts +22 -4
  97. package/lib/yolo-temp-scratch.ts +69 -0
  98. package/package.json +5 -6
  99. package/runtime/gentle-ai-binary.mjs +28 -7
  100. package/runtime/gentle-shell-bundled.mjs +61 -7
  101. package/runtime/gentle-shell-import-pi.mjs +234 -0
  102. package/runtime/gentle-shell-launcher.mjs +144 -10
  103. package/runtime/gentle-shell-uninstall.mjs +18 -1
  104. package/runtime/native-review-cli.mjs +8 -0
  105. package/runtime/review-risk-assessment.mjs +43 -38
  106. package/scripts/build-runtime-modules.mjs +1 -0
  107. package/scripts/bundled-engram-migration.mjs +125 -0
  108. package/scripts/gentle-ai-installer.mjs +10 -10
  109. package/scripts/install-wizard-preview.mjs +2 -2
  110. package/scripts/installer-downloads.mjs +7 -7
  111. package/scripts/installer-preflight.mjs +1 -1
  112. package/scripts/installer-server.mjs +14 -4
  113. package/scripts/main-channel.mjs +3 -2
  114. package/scripts/pi-update.mjs +223 -0
  115. package/scripts/run-test-suite.mjs +5 -0
  116. package/scripts/verify-package-files.mjs +19 -13
  117. package/skills/gentle-ai/SKILL.md +3 -3
  118. package/skills/work-unit-commits/SKILL.md +5 -3
  119. package/tests/agent-risk-escalation.test.ts +37 -10
  120. package/tests/agents-runner.test.ts +260 -3
  121. package/tests/append-system-prompt-route.test.ts +18 -1
  122. package/tests/auto-mode-human-answers.test.ts +68 -0
  123. package/tests/auto-mode-invariants.test.ts +23 -5
  124. package/tests/auto-mode-sdk.test.ts +29 -2
  125. package/tests/auto-permission-answers.test.ts +170 -0
  126. package/tests/auto-permission-classifier.test.ts +353 -2
  127. package/tests/auto-permission-compound.test.ts +24 -0
  128. package/tests/auto-permission-evidence.test.ts +88 -0
  129. package/tests/auto-permission-memory.test.ts +46 -0
  130. package/tests/auto-permission-public-regressions.test.ts +111 -0
  131. package/tests/auto-permission-runtime.test.ts +270 -12
  132. package/tests/autonomous-guard.test.ts +208 -16
  133. package/tests/bounded-writer-admission.test.ts +11 -3
  134. package/tests/bundled-install.test.ts +3 -3
  135. package/tests/card-content-policy.test.ts +19 -9
  136. package/tests/child-safety.test.ts +39 -0
  137. package/tests/codegraph-tools.test.ts +123 -2
  138. package/tests/command-palette.test.ts +14 -4
  139. package/tests/commit-slice-signal.test.ts +162 -0
  140. package/tests/context-backstop.test.ts +159 -0
  141. package/tests/delegate-for-reason-contract.test.ts +91 -24
  142. package/tests/delivered-test-lint.test.ts +105 -0
  143. package/tests/destructive-command-guard.test.ts +85 -1
  144. package/tests/engram-safety.test.ts +35 -7
  145. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  148. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  149. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  150. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  151. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  152. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  153. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  154. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  155. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  156. package/tests/gauge-theme.test.ts +120 -0
  157. package/tests/gentle-agents.test.ts +443 -8
  158. package/tests/gentle-ai-binary.test.ts +1 -1
  159. package/tests/gentle-ai-child-guards.test.ts +61 -0
  160. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  161. package/tests/gentle-ai-installer.test.ts +51 -51
  162. package/tests/gentle-shell-bin.test.ts +180 -17
  163. package/tests/gentle-shell-bundled.test.ts +235 -38
  164. package/tests/gentle-shell-import-pi.test.ts +262 -0
  165. package/tests/gentle-shell-launcher.test.ts +97 -13
  166. package/tests/gentle-shell-uninstall.test.ts +30 -0
  167. package/tests/gentle-shell.test.ts +113 -14
  168. package/tests/gentle-todo.test.ts +258 -27
  169. package/tests/headless-delivery-question.test.ts +58 -0
  170. package/tests/herdr-activity.test.ts +34 -1
  171. package/tests/install-wizard.test.ts +2 -2
  172. package/tests/installer-posix-bootstrap.test.ts +8 -8
  173. package/tests/installer-preflight.test.ts +3 -3
  174. package/tests/installer-probes.test.ts +8 -8
  175. package/tests/installer-runner.test.ts +14 -13
  176. package/tests/installer-server.test.ts +13 -5
  177. package/tests/installer-windows-bootstrap.test.ts +33 -4
  178. package/tests/main-channel-upgrade.test.ts +7 -1
  179. package/tests/native-review-capability-contract.test.ts +7 -1
  180. package/tests/native-review-parity.test.ts +22 -1
  181. package/tests/no-self-pause-contract.test.ts +5 -2
  182. package/tests/odd-integration.test.ts +5 -1
  183. package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
  184. package/tests/odd-routing-contract.test.ts +115 -30
  185. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  186. package/tests/orchestrator-budget.test.ts +19 -8
  187. package/tests/orchestrator-discovery.test.ts +29 -0
  188. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  189. package/tests/orchestrator-scope.test.ts +66 -0
  190. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  191. package/tests/package-manifest.test.ts +28 -24
  192. package/tests/pi-update-launcher.test.ts +106 -0
  193. package/tests/pi-update.test.ts +186 -0
  194. package/tests/primary-session-prompt.test.ts +101 -0
  195. package/tests/provider-contract-bundle.test.ts +118 -0
  196. package/tests/provider-contract-mirror.test.ts +24 -0
  197. package/tests/quiet-tool-rendering.test.ts +23 -0
  198. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  199. package/tests/rdd-status-line.test.ts +131 -0
  200. package/tests/review-agent-end-preflight.test.ts +6 -0
  201. package/tests/review-candidate-view.test.ts +49 -0
  202. package/tests/review-contract-prompt.test.ts +5 -2
  203. package/tests/review-controller-native-routing.test.ts +14 -1
  204. package/tests/review-controller.test.ts +1 -1
  205. package/tests/review-host-relay-routing.test.ts +59 -2
  206. package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
  207. package/tests/review-owner-identity.test.ts +132 -0
  208. package/tests/review-risk-assessment.test.ts +163 -76
  209. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  210. package/tests/review-session-standing-permission.test.ts +27 -1
  211. package/tests/run-test-suite.test.ts +15 -0
  212. package/tests/runtime-harness.mjs +16 -6
  213. package/tests/selection-engine.test.ts +35 -5
  214. package/tests/session-changes-shell.test.ts +1 -1
  215. package/tests/session-profile-freeze.test.ts +37 -1
  216. package/tests/session-profile-persistence.test.ts +334 -0
  217. package/tests/session-scratch.test.ts +945 -0
  218. package/tests/session-subject-routing-contract.test.ts +2 -1
  219. package/tests/shell-bar.test.ts +7 -0
  220. package/tests/shell-changes-view.test.ts +137 -1
  221. package/tests/shell-changes.test.ts +18 -0
  222. package/tests/shell-gauge.test.ts +42 -0
  223. package/tests/shell-todo.test.ts +15 -12
  224. package/tests/small-task-inline-contract.test.ts +37 -0
  225. package/tests/standalone-config-home.test.ts +324 -0
  226. package/tests/startup-banner.test.ts +5 -4
  227. package/tests/startup-home-repo.test.ts +34 -0
  228. package/tests/support/orchestrator-modules.ts +1 -0
  229. package/tests/task-size-routing-contract.test.ts +69 -9
  230. package/tests/test-scratch-contract.test.ts +102 -0
  231. package/tests/test-strength-contract.test.ts +36 -1
  232. package/tests/tool-batching-contract.test.ts +64 -0
  233. package/tests/verification-proportionality.test.ts +46 -0
  234. package/tests/verifier-admission.test.ts +137 -0
  235. package/tests/verify-agent-contract.test.ts +8 -8
  236. package/tests/vim-editor-adapter.test.ts +41 -15
  237. package/tests/windows-hidden-processes.test.ts +5 -0
  238. package/tests/writer-edit-surface-scope.test.ts +3 -0
  239. package/tests/writer-feature-document-admission.test.ts +156 -0
  240. package/tests/yolo-mode.test.ts +24 -1
  241. package/tests/yolo-temp-scratch.test.ts +71 -0
  242. package/scripts/mirror-odd-routing.mjs +0 -242
  243. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  244. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  245. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  246. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  247. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  248. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  249. /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
- import { recognizeDestructiveCommands } from "../lib/destructive-command-guard.ts";
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 }]),
@@ -1098,9 +1143,11 @@ function homeRootedRepository(cwd: string): string | undefined {
1098
1143
  * `NativeReviewModeStatus`) that the `/gentle:review-mode` command also
1099
1144
  * calls. Never throws and never hangs past `signal`'s deadline when one is
1100
1145
  * given: an absent binary, a timed-out/aborted process, or a native CLI
1101
- * failure all resolve to `undefined`. `ctx` is optional and used only for a
1102
- * one-shot (per process) UI notice when the read is swallowed, so a
1103
- * sustained native outage is observable beyond the rendered "unknown" line.
1146
+ * failure all resolve to `undefined` unless an earlier read for the same cwd
1147
+ * succeeded, in which case that last known status is kept (gentle-shell#1029:
1148
+ * the rendered line sits in the cached prompt prefix). `ctx` is optional and
1149
+ * used only for a one-shot (per process) UI notice when the read is
1150
+ * swallowed, so a sustained native outage is observable beyond the prompt.
1104
1151
  */
1105
1152
  async function resolveRddModeStatus(
1106
1153
  nativeReviewCli: Pick<NativeReviewCli, "reviewMode"> | null | undefined,
@@ -1112,13 +1159,20 @@ async function resolveRddModeStatus(
1112
1159
  const nowMs = now();
1113
1160
  const cached = rddStatusMemo.get(cwd);
1114
1161
  if (cached !== undefined && cached.expiresAt > nowMs) return cached.status;
1115
- const status = await readRddModeStatusOnce(nativeReviewCli, cwd, signal);
1162
+ // gentle-shell#1029: the rendered line is part of the cached system-prompt
1163
+ // prefix. A re-read that fails or times out keeps the last successfully
1164
+ // read status instead of flipping the prompt to "unknown" and back, which
1165
+ // would invalidate the provider's prefix cache for the whole conversation.
1166
+ // Only a status the native reader actually reports replaces a known one.
1167
+ const outcome = await readRddModeStatusWithCause(nativeReviewCli, cwd, signal);
1168
+ const read = "status" in outcome ? outcome.status : undefined;
1169
+ const status = read ?? cached?.status;
1116
1170
  rddStatusMemo.set(cwd, { status, expiresAt: nowMs + RDD_STATUS_MEMO_TTL_MS });
1117
- if (status === undefined && !rddStatusUnavailableWarned) {
1171
+ if ("cause" in outcome && !rddStatusUnavailableWarned) {
1118
1172
  rddStatusUnavailableWarned = true;
1119
1173
  if (ctx?.hasUI) {
1120
1174
  ctx.ui.notify(
1121
- "Gentle AI: receipt-driven-development status is unavailable (native review CLI absent, timed out, or failed). The parent prompt renders \"unknown\" until this recovers; 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.`,
1122
1176
  "warning",
1123
1177
  );
1124
1178
  }
@@ -1131,6 +1185,46 @@ function isRddStatusLineOff(line: string | undefined): boolean {
1131
1185
  return line !== undefined && line.startsWith("Receipt-driven development: off ");
1132
1186
  }
1133
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
+
1134
1228
  /** Resolves and renders the RDD status line for a production call site in one call. */
1135
1229
  async function resolveRddStatusLine(
1136
1230
  nativeReviewCli: Pick<NativeReviewCli, "reviewMode"> | null | undefined,
@@ -1197,13 +1291,13 @@ function renderOrchestratorPrompt(
1197
1291
  // pinned at an 8192-byte budget (tests/orchestrator-budget.test.ts).
1198
1292
  const PROVIDER_CONTRACT_MIRROR_ROOT = join(PACKAGE_ROOT, "contracts", "review-provider-contract-mirror");
1199
1293
  const PROVIDER_CONTRACT_LOCK_FILE = "provider-contract.lock.json";
1200
- const PI_ORCHESTRATION_RUNTIME = "pi";
1294
+ const PI_MIRROR_RUNTIME = "pi";
1201
1295
 
1202
1296
  let reviewContractPromptFragmentCache: string | null | undefined;
1203
1297
  let reviewContractPromptMissingWarned = false;
1204
1298
 
1205
- // Verifies the mirrored orchestration/pi.md bytes against the lock's digest before injection (gentle-ai R1/R3).
1206
- 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 {
1207
1301
  try {
1208
1302
  const lockPath = join(mirrorRoot, PROVIDER_CONTRACT_LOCK_FILE);
1209
1303
  const lock = JSON.parse(readFileSync(lockPath, "utf8")) as {
@@ -1211,20 +1305,71 @@ function readMirroredReviewContractFragment(mirrorRoot: string = PROVIDER_CONTRA
1211
1305
  entries?: Record<string, unknown>;
1212
1306
  };
1213
1307
  if (typeof lock.contract_semver !== "string" || lock.contract_semver === "") return null;
1214
- const expectedSha256 = lock.entries?.[`orchestration/${PI_ORCHESTRATION_RUNTIME}.md`];
1308
+ const expectedSha256 = lock.entries?.[bundlePath];
1215
1309
  if (typeof expectedSha256 !== "string" || !/^[0-9a-f]{64}$/.test(expectedSha256)) return null;
1216
- const contractPath = join(mirrorRoot, `v${lock.contract_semver}`, "bundle", "orchestration", `${PI_ORCHESTRATION_RUNTIME}.md`);
1217
- const rawBytes = readFileSync(contractPath);
1310
+ const filePath = join(mirrorRoot, `v${lock.contract_semver}`, "bundle", ...bundlePath.split("/"));
1311
+ const rawBytes = readFileSync(filePath);
1218
1312
  const actualSha256 = createHash("sha256").update(rawBytes).digest("hex");
1219
1313
  if (!timingSafeEqual(Buffer.from(expectedSha256, "hex"), Buffer.from(actualSha256, "hex"))) return null;
1220
- const text = rawBytes.toString("utf8").trim();
1221
- if (text.length === 0) return null;
1222
- 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") };
1223
1315
  } catch {
1224
1316
  return null;
1225
1317
  }
1226
1318
  }
1227
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
+
1228
1373
  function loadReviewContractPromptFragment(
1229
1374
  ctx: Pick<ExtensionContext, "hasUI" | "ui">,
1230
1375
  mirrorRoot: string = PROVIDER_CONTRACT_MIRROR_ROOT,
@@ -1306,34 +1451,44 @@ ${personaPrompt}
1306
1451
 
1307
1452
  ${languageBoundary}
1308
1453
 
1454
+ ## Authorized work continuity
1455
+
1456
+ An authorized outcome includes its necessary in-scope implementation, documentation and verification steps. Continue those steps without asking the human to approve each transition. Never end a turn merely announcing an immediately executable authorized next step; execute it instead. A progress update is not a request for permission.
1457
+
1458
+ Stop only for an unresolved consequential scope/product decision, required explicit consent, a denied action, unavailable required proof, or the human's explicit stop. Do not infer permission for publishing, destructive actions, unrelated scope or native consent from this continuity rule. Report a concrete blocker and the exact decision needed rather than requesting a generic "continue".
1459
+
1460
+ For background work, launch the bounded action and end the turn when no independent in-scope work remains. Retain its identity and resume the already-authorized continuation automatically on its completion event; do not ask the human to say "continue", poll, or claim completion while checks remain pending.
1461
+
1309
1462
  Default workflow: Organic Driven Development (MANDATORY)
1310
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:
1311
- 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.
1312
- 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.
1313
- 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.
1314
- 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.
1315
- 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.
1316
- 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.
1317
- 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.
1318
- 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.
1319
- 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()}
1320
1465
 
1321
1466
  Harness principles:
1322
1467
  - el Gentleman is not prompt engineering. It is runtime discipline around powerful agents.
1323
- - 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.
1324
1468
  - Clarify scope, constraints, acceptance criteria, and non-goals before implementation.
1325
1469
  - Use subagents when available for exploration, planning, implementation, and review, while keeping one parent session responsible for orchestration.
1326
- - Parallel writers only with disjoint Allowed edit surfaces (runtime-enforced) or isolated worktrees.
1327
- - 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.
1328
1471
  - Protect the human reviewer: avoid oversized changes, surface review workload risk, and ask before turning one task into a large multi-area change.
1329
1472
  - Never claim persistent memory is available because of this package. Memory is provided by separate packages or MCP tools when installed and callable.
1330
1473
 
1331
- ${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.";
1332
1485
  }
1333
1486
 
1334
1487
  // Matches `git [global-flags] push` — tolerates flags like -C /repo or --work-tree=/tmp
1335
1488
  // between `git` and the subcommand. Short flags may be followed by a separate value token.
1336
- const GIT_GLOBAL_FLAGS_SRC = String.raw`(?:\s+--?\S+(?:\s+[^-\s]\S*)?)* `;
1489
+ // gentle-shell#2172: `\s+` rather than a literal space, so a tab-separated
1490
+ // `git\tpush` is guarded like any other spelling.
1491
+ const GIT_GLOBAL_FLAGS_SRC = String.raw`(?:\s+--?\S+(?:\s+[^-\s]\S*)?)*\s+`;
1337
1492
  const GIT_PUSH_RE = new RegExp(String.raw`\bgit${GIT_GLOBAL_FLAGS_SRC}(push)\b`);
1338
1493
 
1339
1494
  const DENIED_BASH_PATTERNS: RegExp[] = [
@@ -1399,7 +1554,7 @@ interface LoadGuardrailsOptions {
1399
1554
 
1400
1555
  const GUARDED_KEY_PATTERNS: Record<GuardedCommandKey, RegExp> = {
1401
1556
  gitPush: GIT_PUSH_RE,
1402
- gitRebase: /\bgit\s+(rebase)\b/,
1557
+ gitRebase: new RegExp(String.raw`\bgit${GIT_GLOBAL_FLAGS_SRC}(rebase)\b`),
1403
1558
  gitBranchDeleteForce: /\bgit\s+(branch)\s+(?:-[a-zA-Z]*D[a-zA-Z]*|-[a-zA-Z]*d[a-zA-Z]*f[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*d[a-zA-Z]*|--delete\b[^\r\n;&|]*--force\b|--force\b[^\r\n;&|]*--delete\b)/,
1404
1559
  npmPublish: /\bnpm\s+(publish)\b/,
1405
1560
  piRemove: /\bpi\s+(remove)\b/,
@@ -1441,12 +1596,15 @@ function collectGuardedMatches(
1441
1596
  config: RuntimeGuardrailsConfig,
1442
1597
  ): GuardMatch[] {
1443
1598
  const matches: GuardMatch[] = [];
1599
+ const dataRanges = dataArgumentRanges(command);
1444
1600
  for (const [key, pattern] of Object.entries(GUARDED_KEY_PATTERNS) as [
1445
1601
  GuardedCommandKey,
1446
1602
  RegExp,
1447
1603
  ][]) {
1448
1604
  const globalPattern = new RegExp(pattern.source, `${pattern.flags}g`);
1449
1605
  for (const match of command.matchAll(globalPattern)) {
1606
+ // gentle-shell#2174: text inside a data command's argument is not an operation.
1607
+ if (inDataArgument(dataRanges, match.index)) continue;
1450
1608
  const action = config.autonomousMode
1451
1609
  ? (config.guardedCommands[key] ?? AUTONOMOUS_DEFAULT_ACTIONS[key])
1452
1610
  : "confirm";
@@ -1460,6 +1618,10 @@ function collectGuardedMatches(
1460
1618
  return matches.sort((left, right) => left.triggerIndex - right.triggerIndex);
1461
1619
  }
1462
1620
 
1621
+ function inDataArgument(ranges: ReadonlyArray<readonly [number, number]>, index: number): boolean {
1622
+ return ranges.some(([start, end]) => index >= start && index < end);
1623
+ }
1624
+
1463
1625
  function evaluateGuardedCommand(
1464
1626
  command: string,
1465
1627
  config: RuntimeGuardrailsConfig,
@@ -1470,8 +1632,10 @@ function evaluateGuardedCommand(
1470
1632
  if (hardDeny) return { action: "block", triggerIndex: hardDeny.triggerIndex, matches };
1471
1633
 
1472
1634
  // Hard denies override every configured action across the complete command.
1635
+ const dataRanges = dataArgumentRanges(command);
1473
1636
  for (const pattern of DENIED_BASH_PATTERNS) {
1474
- const denied = pattern.exec(command);
1637
+ const denied = [...command.matchAll(new RegExp(pattern.source, `${pattern.flags.replace("g", "")}g`))]
1638
+ .find((candidate) => !inDataArgument(dataRanges, candidate.index));
1475
1639
  if (!denied) continue;
1476
1640
  const matchedAction = matches.find((match) =>
1477
1641
  match.triggerIndex >= denied.index && match.triggerIndex < denied.index + denied[0].length,
@@ -1817,14 +1981,76 @@ function createHerdrConfirmationLifecycle(events: ExtensionAPI["events"]): Herdr
1817
1981
  };
1818
1982
  }
1819
1983
 
1820
- /** Conservative waiver surface: one plain push in the current repository, no shell/wrapper or destination-changing options. */
1821
- function isOrdinaryYoloPush(command: string, evaluation: GuardEvaluation): boolean {
1822
- if (evaluation.action !== "confirm" || evaluation.dataLoss || evaluation.matches.length !== 1 || evaluation.key !== "gitPush") return false;
1823
- return /^git\s+push(?:\s+(?:-u|--set-upstream|[A-Za-z0-9_][A-Za-z0-9_./-]*))*\s*$/.test(command);
1984
+ // gentle-shell#2172: YOLO waives reversible delivery, not only one bare push.
1985
+ // A guarded segment is judged from its trigger word (`push`/`rebase`) to the
1986
+ // next command separator; a plain token or allow-listed option list is the
1987
+ // whole eligible grammar, so quotes, wrappers and unknown options fall back
1988
+ // to the ordinary confirmation. Everything that deletes or rewrites a remote
1989
+ // ref, publishes, or runs arbitrary commands is deliberately absent:
1990
+ // `--delete`/`-d`, `:dst` and `+src` refspecs, `--mirror`, `--prune`,
1991
+ // `--all`, every force spelling, `--receive-pack`/`--exec`, `--repo`, `-o`.
1992
+ const YOLO_WAIVABLE_KEYS: ReadonlySet<GuardedCommandKey> = new Set(["gitPush", "gitRebase"]);
1993
+ const YOLO_SEGMENT_END_RE = /&&|\|\||;|\||\n|\r/;
1994
+ // Text between the segment start and the `push`/`rebase` word: optional
1995
+ // variable assignments, `env`/`command` prefixes and the `git` word with at
1996
+ // most a `-C <path>`. Other global options (`-c key=value`, `--exec-path`,
1997
+ // `--git-dir`) can run or redirect code, so they keep the confirmation.
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+$/;
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)`);
2008
+
2009
+ function yoloGuardedSegment(command: string, triggerIndex: number): string {
2010
+ const rest = command.slice(triggerIndex);
2011
+ const end = rest.search(YOLO_SEGMENT_END_RE);
2012
+ return end < 0 ? rest : rest.slice(0, end);
2013
+ }
2014
+
2015
+ function yoloSegmentPrefix(command: string, triggerIndex: number): string {
2016
+ const head = command.slice(0, triggerIndex);
2017
+ const separators = [...head.matchAll(new RegExp(YOLO_SEGMENT_END_RE.source, "g"))];
2018
+ const last = separators.at(-1);
2019
+ return last === undefined ? head : head.slice(last.index + last[0].length);
1824
2020
  }
1825
2021
 
1826
- /** Preserve explicitly configured confirmations even when legacy env/autonomy resolution ignores them. */
1827
- function yoloPushConfiguredRestriction(cwd: string, options: LoadGuardrailsOptions = {}): GuardAction | undefined {
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;
2032
+ for (const match of evaluation.matches) {
2033
+ if (!YOLO_WAIVABLE_KEYS.has(match.key)) return false;
2034
+ if (!YOLO_GIT_PREFIX_RE.test(yoloSegmentPrefix(command, match.triggerIndex))) return false;
2035
+ const segment = yoloGuardedSegment(command, match.triggerIndex);
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;
2038
+ }
2039
+ // The data-loss recognizer leaves non-hard Git effects (reset, stash drop,
2040
+ // restore, checkout --) to the primary policy; under YOLO only a rebase that
2041
+ // a guarded match already vetted may remain.
2042
+ const rebaseTriggers = new Set(evaluation.matches.filter((match) => match.key === "gitRebase").map((match) => match.triggerIndex));
2043
+ for (const [index, effect] of effects.entries()) {
2044
+ if (scratch[index]) continue;
2045
+ if (effect.hardDeny || effect.kind !== "git") return false;
2046
+ const rebase = /^git(?:\s+(?:-C|-c|--git-dir|--work-tree|--namespace)\s+\S+|\s+-\S+)*\s+(rebase)\b/.exec(command.slice(effect.triggerIndex));
2047
+ if (rebase === null || !rebaseTriggers.has(effect.triggerIndex + rebase.index + rebase[0].lastIndexOf("rebase"))) return false;
2048
+ }
2049
+ return true;
2050
+ }
2051
+
2052
+ /** Preserve explicitly configured confirmations for the waivable keys present, even when legacy env/autonomy resolution ignores them. */
2053
+ function yoloConfiguredRestriction(cwd: string, keys: readonly GuardedCommandKey[], options: LoadGuardrailsOptions = {}): GuardAction | undefined {
1828
2054
  let restriction: GuardAction | undefined;
1829
2055
  for (const path of [join(options.gentlePiConfigHome ?? gentleAiConfigHome(), "runtime-guardrails.json"), join(cwd, ".pi", "gentle-ai", "runtime-guardrails.json")]) {
1830
2056
  try {
@@ -1834,9 +2060,11 @@ function yoloPushConfiguredRestriction(cwd: string, options: LoadGuardrailsOptio
1834
2060
  restriction ??= "confirm";
1835
2061
  continue;
1836
2062
  }
1837
- const action = isRecord(parsed.guardedCommands) ? parsed.guardedCommands.gitPush : undefined;
1838
- if (action === "block") return "block";
1839
- if (action !== undefined && action !== "allow") restriction = "confirm";
2063
+ for (const key of keys) {
2064
+ const action = isRecord(parsed.guardedCommands) ? parsed.guardedCommands[key] : undefined;
2065
+ if (action === "block") return "block";
2066
+ if (action !== undefined && action !== "allow") restriction = "confirm";
2067
+ }
1840
2068
  } catch { restriction ??= "confirm"; }
1841
2069
  }
1842
2070
  return restriction;
@@ -1855,8 +2083,9 @@ async function confirmCommand(
1855
2083
  // Configuration provenance is distinct from legacy autonomy resolution, which
1856
2084
  // can ignore confirmations when an environment override is present. Activation
1857
2085
  // cannot waive a restriction from either user or project configuration layer.
1858
- const configuredRestriction = yoloActive && evaluation.matches.some((match) => match.key === "gitPush")
1859
- ? yoloPushConfiguredRestriction(ctx.cwd) : undefined;
2086
+ const waivableKeys = evaluation.matches.map((match) => match.key).filter((key) => YOLO_WAIVABLE_KEYS.has(key));
2087
+ const configuredRestriction = yoloActive && waivableKeys.length > 0
2088
+ ? yoloConfiguredRestriction(ctx.cwd, waivableKeys) : undefined;
1860
2089
  const classification = evaluation.action === "block" || configuredRestriction === "block"
1861
2090
  ? "block" : configuredRestriction ?? evaluation.action;
1862
2091
 
@@ -1892,7 +2121,7 @@ async function confirmCommand(
1892
2121
  }
1893
2122
 
1894
2123
  // Full T1 evaluation has already run. YOLO never changes persistent autonomy.
1895
- if (yoloActive && configuredRestriction === undefined && isOrdinaryYoloPush(command, evaluation)) return undefined;
2124
+ if (yoloActive && configuredRestriction === undefined && isReversibleYoloCommand(command, evaluation)) return undefined;
1896
2125
 
1897
2126
  // classification === "confirm"
1898
2127
  if (!ctx.hasUI) {
@@ -5064,7 +5293,7 @@ const REVIEW_CONTROLLER_PARAMETER_FIELDS = {
5064
5293
  },
5065
5294
  input: {
5066
5295
  type: "string",
5067
- 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.",
5068
5297
  },
5069
5298
  outputPath: { type: "string", description: "Retired with legacy bundle export; ignored. Export returns legacy-operation-retired." },
5070
5299
  inputPath: { type: "string", description: "Repository-local JSON input file for the separate legacy controller flow (alternative to input). Legacy bundle import is retired." },
@@ -6535,6 +6764,8 @@ const processRetainedNativeStatusSelections = new Map<PendingReviewConsentSessio
6535
6764
  // named-agent start increments the depth, a matching end decrements it,
6536
6765
  // and a fresh primary-loop start resets it to 0.
6537
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>();
6538
6769
 
6539
6770
  // gentle-shell#1064 slice 1: the parent-session profile binding store lives in
6540
6771
  // lib/session-profile-binding.ts (in-process, keyed by parent session id). The
@@ -7914,6 +8145,20 @@ function isSelectedReviewCapture(value: SelectedReviewCapture | Record<string, u
7914
8145
 
7915
8146
  function captureGroupRejected(reason: string): Record<string, unknown> { return captureBindingRejected(reason, true); }
7916
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
+
7917
8162
  function hasExactReviewCaptureSuffix(status: ReviewStatusV3, expected: readonly string[]): boolean {
7918
8163
  const current = status.nextTransition?.kind === "collect"
7919
8164
  ? (status.nextTransition.collect?.inputs ?? []).filter((input) => input.captureOperation === "review.capture-result").map(canonicalReviewCaptureBinding)
@@ -8220,7 +8465,7 @@ async function executeReviewCaptureGroupOperation(
8220
8465
  if (negotiated.transport !== undefined) return hostTransportUnavailable("gentle_review_capture_group", negotiated.transport);
8221
8466
  status = negotiated.status!;
8222
8467
  } catch (error) {
8223
- return { ...captureGroupRejected(error instanceof Error ? error.message : String(error)), outcome: "native-status-failed" };
8468
+ return captureGroupStatusFailure(error);
8224
8469
  }
8225
8470
  const group = selectExactReviewCaptureGroup(status, parameters.lineageId, canonicalBindings);
8226
8471
  if (!isSelectedReviewCaptureGroup(group)) return group;
@@ -8266,7 +8511,7 @@ async function executeReviewCaptureGroupOperation(
8266
8511
  if (!hasExactReviewCaptureSuffix(negotiated.status!, canonicalBindings.slice(index))) return { ...captureGroupAuthorityDrift(negotiated.status!), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
8267
8512
  current = selectExactReviewCapture(negotiated.status!, parameters.lineageId, canonicalBindings[index]!);
8268
8513
  } catch (error) {
8269
- 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) };
8270
8515
  }
8271
8516
  if (!isSelectedReviewCapture(current)) return { ...captureGroupRejected(String(current.reason ?? "current STATUS rejected a reviewer binding")), ...reviewHostRelayGroupProgress(group.slots, prepared, index) };
8272
8517
  try {
@@ -9384,8 +9629,8 @@ export const __testing = {
9384
9629
  guardedCommandPreview,
9385
9630
  guardedCommandTitle,
9386
9631
  loadRuntimeGuardrailsConfig,
9387
- isOrdinaryYoloPush,
9388
- yoloPushConfiguredRestriction,
9632
+ isReversibleYoloCommand,
9633
+ yoloConfiguredRestriction,
9389
9634
  buildGentlePrompt,
9390
9635
  nativeStatusUnsupported,
9391
9636
  nativeStartRejection,
@@ -9401,6 +9646,7 @@ export const __testing = {
9401
9646
  renderOrchestratorPrompt,
9402
9647
  loadReviewContractPromptFragment,
9403
9648
  readMirroredReviewContractFragment,
9649
+ readMirroredOddProtocolSteps,
9404
9650
  loadBackgroundSubagentsPolicy,
9405
9651
  resolveBackgroundSubagentsPolicy,
9406
9652
  renderBackgroundSubagentsReport,
@@ -9410,6 +9656,7 @@ export const __testing = {
9410
9656
  readActiveToolNames,
9411
9657
  renderBackgroundSubagentsStatusLine,
9412
9658
  renderRddStatusLine,
9659
+ pinRddStatusLine,
9413
9660
  isValidRddModeStatus,
9414
9661
  resolveRddModeStatus,
9415
9662
  resolveRddStatusLine,
@@ -9487,6 +9734,9 @@ function createGentleAiExtensionForTesting(
9487
9734
  cancel: () => classifier?.cancel(),
9488
9735
  }, { configHome: gentlePiConfigHome(permissionEnvironment) });
9489
9736
  const auto = autoPermissions;
9737
+ pi.on("tool_result", (event, ctx) => {
9738
+ if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1") auto.toolResult(event, ctx);
9739
+ });
9490
9740
  pi.on("input", (event, ctx) => {
9491
9741
  if (permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1") auto.input(event, ctx);
9492
9742
  return { action: "continue" };
@@ -9528,11 +9778,17 @@ function createGentleAiExtensionForTesting(
9528
9778
  reviewSidebar.reset(ctx);
9529
9779
  statusTiming.reset(ctx);
9530
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;
9531
9785
  let reminderSessionActive = true;
9532
9786
  let reminderEpoch = 0;
9533
9787
  let reminderManager: ExtensionContext["sessionManager"] | undefined;
9534
9788
  let unbindPreparation: (() => void) | undefined;
9535
9789
  pi.on("session_shutdown", (event, context) => {
9790
+ pinnedRddStatusLines.delete(pendingReviewConsentSessionKey(context, pendingReviewConsentFallbackKey));
9791
+ rddOffReviewLoadout = false;
9536
9792
  auto.reset();
9537
9793
  yolo.reset(context);
9538
9794
  reviewSidebar.reset();
@@ -9780,10 +10036,20 @@ function createGentleAiExtensionForTesting(
9780
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.",
9781
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.",
9782
10038
  "Use gentle_review only for native review authority operations; delivery commands follow ordinary repository policy.",
9783
- '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.',
9784
10040
  ],
9785
10041
  parameters: REVIEW_CONTROLLER_PARAMETERS,
9786
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
+ },
9787
10053
  renderCall(args, theme, context) {
9788
10054
  return renderGentleAiLifecycleCall(
9789
10055
  reviewToolOperationPath(args),
@@ -10051,7 +10317,8 @@ function createGentleAiExtensionForTesting(
10051
10317
  // this branch, so no line is resolved or computed for them.
10052
10318
  // resolveRddStatusLine never throws and never hangs past
10053
10319
  // RDD_STATUS_TIMEOUT_MS: an absent/timed-out/aborted/failing native
10054
- // binary renders the fail-closed "unknown" line instead.
10320
+ // binary keeps the last known line for this cwd, or renders the
10321
+ // fail-closed "unknown" line when none was ever read (gentle-shell#1029).
10055
10322
  const rddStatusSignal = AbortSignal.timeout(RDD_STATUS_TIMEOUT_MS);
10056
10323
  // A startup read still in flight fills the memo: wait for it within the
10057
10324
  // same deadline instead of spawning the same STATUS a second time.
@@ -10059,7 +10326,13 @@ function createGentleAiExtensionForTesting(
10059
10326
  if (inFlightModeRead !== undefined) await Promise.race([inFlightModeRead, rddAbortRejection(rddStatusSignal)]).catch(() => undefined);
10060
10327
  const rddStatusLine = !isPrimarySession
10061
10328
  ? undefined
10062
- : 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);
10063
10336
  const gentlePrompt = rddStatusLine === undefined
10064
10337
  ? ""
10065
10338
  : `\n\n${buildGentlePrompt(
@@ -10142,7 +10415,7 @@ function createGentleAiExtensionForTesting(
10142
10415
  consumeReviewMutation(pi, ctx.sessionManager, root, mutation, "nudged", targetIdentity);
10143
10416
  });
10144
10417
 
10145
- pi.on("tool_result", async (event, ctx) => {
10418
+ const recordWriteResult = async (event: ToolResultEvent, ctx: ExtensionContext): Promise<void> => {
10146
10419
  if (!reminderSessionActive || (reminderManager && reminderManager !== ctx.sessionManager) || event.isError !== false || (event.toolName !== "write" && event.toolName !== "edit")) return;
10147
10420
  if (!isRecord(event.input) || typeof event.input.path !== "string" || !event.input.path.trim()) return;
10148
10421
  try {
@@ -10154,9 +10427,33 @@ function createGentleAiExtensionForTesting(
10154
10427
  if (root) recordReviewMutation(pi, ctx.sessionManager, root, { source: "direct", toolName: event.toolName, toolCallId: event.toolCallId, ...directWriterProfile(pi, ctx) });
10155
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);
10156
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);
10157
10449
  });
10158
10450
 
10159
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
+ }
10160
10457
  const primaryToolCall = permissionEnvironment.GENTLE_PI_AGENTS_CHILD !== "1" && (processAgentEndSubagentDepth.get(pendingReviewConsentSessionKey(ctx, pendingReviewConsentFallbackKey)) ?? 0) === 0;
10161
10458
  const yoloActive = primaryToolCall && await yolo.active(ctx);
10162
10459
  const sensitivePathDenied = evaluateSensitivePathTool(
@@ -10164,11 +10461,22 @@ function createGentleAiExtensionForTesting(
10164
10461
  event.input,
10165
10462
  );
10166
10463
  if (sensitivePathDenied) return sensitivePathDenied;
10464
+ const deliveredTestDenied = evaluateDeliveredTestTool(event.toolName, event.input, ctx.cwd);
10465
+ if (deliveredTestDenied) return deliveredTestDenied;
10167
10466
  if (event.toolName === "subagent_run") {
10168
10467
  const judgmentDayFixDenied = rejectInvalidJudgmentDayFixDispatch(event.input);
10169
10468
  if (judgmentDayFixDenied) return judgmentDayFixDenied;
10170
10469
  const writerScopeDenied = rejectUnscopedBoundedWriterDispatch(event.input);
10171
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
+ }
10172
10480
  try {
10173
10481
  injectReviewCandidateView(event.input, candidateViews);
10174
10482
  return undefined;
@@ -10322,6 +10630,10 @@ function createGentleAiExtensionForTesting(
10322
10630
  if (modelConfig.status === "invalid") {
10323
10631
  lines.push(`remedy: fix or remove ${modelConfig.path}`);
10324
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
+ }));
10325
10637
  ctx.ui.notify(
10326
10638
  lines.join("\n"),
10327
10639
  lines.some((line) => line.startsWith("fail:")) || assetLines.some((line) => line.startsWith("warn:")) ? "warning" : "info",