gentle-pi 3.7.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/README.md +37 -6
  2. package/assets/agents/gentle-ai-explore.md +4 -4
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -9
  5. package/assets/orchestrator-delegation.md +40 -41
  6. package/assets/orchestrator-memory.md +1 -22
  7. package/assets/orchestrator-skills.md +1 -1
  8. package/assets/orchestrator.md +12 -24
  9. package/assets/support/strict-tdd-verify.md +4 -266
  10. package/assets/support/strict-tdd.md +8 -360
  11. package/bin/gentle-shell.mjs +254 -29
  12. package/docs/delegated-verification.md +26 -1
  13. package/docs/gentle-agents-activity.md +24 -0
  14. package/docs/gentle-shell.md +92 -28
  15. package/docs/native-authority-architecture.md +2 -2
  16. package/docs/prompt-history.md +280 -0
  17. package/docs/readme-reference.md +112 -206
  18. package/docs/telemetry.md +1 -1
  19. package/docs/yolo-mode.md +86 -0
  20. package/extensions/child-context.ts +26 -0
  21. package/extensions/child-safety.ts +23 -0
  22. package/extensions/gentle-agents.ts +429 -262
  23. package/extensions/gentle-ai.ts +781 -683
  24. package/extensions/gentle-shell.ts +1375 -88
  25. package/extensions/gentle-stats.ts +101 -0
  26. package/extensions/gentle-todo.ts +18 -7
  27. package/extensions/history/atomic-write.ts +38 -0
  28. package/extensions/history/hide-prompts.ts +183 -0
  29. package/extensions/history/index.ts +1419 -0
  30. package/extensions/history/load-shared-history.ts +39 -0
  31. package/extensions/history/selector-helpers.ts +538 -0
  32. package/extensions/history/session-scan.ts +233 -0
  33. package/extensions/history/store.ts +1119 -0
  34. package/extensions/nan-provider.ts +6 -0
  35. package/extensions/quiet-tools.ts +179 -87
  36. package/extensions/resume-hint.ts +60 -0
  37. package/extensions/skill-registry.ts +16 -12
  38. package/extensions/startup-banner.ts +60 -31
  39. package/lib/agent-assets.ts +604 -0
  40. package/lib/agent-profile-pin.ts +12 -0
  41. package/lib/agents-message-delivery.ts +181 -0
  42. package/lib/agents-protocol.ts +60 -0
  43. package/lib/agents-runner.ts +96 -98
  44. package/lib/agents-view.ts +26 -3
  45. package/lib/agents-widget.ts +16 -9
  46. package/lib/append-system-prompt.ts +21 -0
  47. package/lib/bounded-writer-admission.ts +147 -0
  48. package/lib/card-style-policy.ts +60 -0
  49. package/lib/child-context-files.ts +166 -0
  50. package/lib/codemode-renderer.ts +185 -0
  51. package/lib/command-palette-catalog.ts +3 -9
  52. package/lib/command-palette.ts +25 -14
  53. package/lib/destructive-command-guard.ts +144 -0
  54. package/lib/gentle-ai-elapsed-store.ts +87 -0
  55. package/lib/gentle-ai-renderer.ts +196 -39
  56. package/lib/gentle-shell-launcher.ts +24 -13
  57. package/lib/gentle-shell-resume-hint.ts +176 -0
  58. package/lib/history-capture-policy.ts +95 -0
  59. package/lib/model-routing-authority.ts +5 -1
  60. package/lib/nan-provider.ts +227 -0
  61. package/lib/native-review-cli.ts +49 -108
  62. package/lib/odd-phase-inference.ts +231 -0
  63. package/lib/odd-phase.ts +141 -0
  64. package/lib/overlay-repaint.ts +26 -0
  65. package/lib/pi-tui-keys.ts +53 -0
  66. package/lib/review-candidate-view-owner.ts +67 -17
  67. package/lib/review-candidate-view.ts +112 -25
  68. package/lib/review-reminder-receipt.ts +48 -8
  69. package/lib/review-risk-assessment.ts +156 -11
  70. package/lib/review-sidebar-state.ts +223 -0
  71. package/lib/selection-engine.ts +515 -0
  72. package/lib/session-messaging-grants.ts +135 -0
  73. package/lib/session-worktree-registry.ts +14 -2
  74. package/lib/shell-bar.ts +179 -24
  75. package/lib/shell-card.ts +287 -18
  76. package/lib/shell-changes-view.ts +2 -1
  77. package/lib/shell-prompt.ts +102 -6
  78. package/lib/shell-sidebar-layout.ts +78 -14
  79. package/lib/shell-sidebar.ts +15 -1
  80. package/lib/shell-todo.ts +23 -13
  81. package/lib/shell-usage-view.ts +9 -4
  82. package/lib/shell-usage.ts +66 -10
  83. package/lib/stats-collector.ts +381 -0
  84. package/lib/stats-view.ts +431 -0
  85. package/lib/theme-customization.ts +52 -0
  86. package/lib/vim-editor-adapter.ts +379 -0
  87. package/lib/vim-normal-engine.ts +154 -0
  88. package/lib/vim-operator-engine.ts +416 -0
  89. package/lib/vim-policy.ts +49 -0
  90. package/lib/vim-visual-engine.ts +107 -0
  91. package/lib/visual-customization-policy.ts +108 -0
  92. package/lib/visual-customize-view.ts +330 -0
  93. package/lib/visual-profiles.ts +228 -0
  94. package/lib/yolo-session-policy.ts +240 -0
  95. package/package.json +20 -8
  96. package/runtime/gentle-shell-launcher.mjs +23 -12
  97. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  98. package/runtime/native-review-cli.mjs +49 -108
  99. package/runtime/review-risk-assessment.mjs +154 -9
  100. package/scripts/build-runtime-modules.mjs +1 -0
  101. package/scripts/gentle-ai-installer.mjs +14 -13
  102. package/scripts/mirror-odd-routing.mjs +2 -2
  103. package/scripts/run-test-suite.mjs +76 -0
  104. package/scripts/test-packed-runner.mjs +31 -14
  105. package/scripts/verify-package-files.mjs +11 -22
  106. package/skills/branch-pr/SKILL.md +24 -52
  107. package/skills/chained-pr/SKILL.md +31 -15
  108. package/skills/chained-pr/references/chaining-details.md +31 -20
  109. package/skills/gentle-ai/SKILL.md +9 -15
  110. package/skills/issue-creation/SKILL.md +8 -2
  111. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  112. package/skills/work-unit-commits/SKILL.md +4 -3
  113. package/tests/agent-profiles.test.ts +18 -0
  114. package/tests/agents-fake-child.ts +2 -2
  115. package/tests/agents-message-delivery.test.ts +106 -0
  116. package/tests/agents-protocol.test.ts +40 -0
  117. package/tests/agents-runner.test.ts +378 -89
  118. package/tests/agents-view-thread-identity.test.ts +169 -0
  119. package/tests/agents-view.test.ts +8 -2
  120. package/tests/agents-widget.test.ts +154 -15
  121. package/tests/append-system-prompt-route.test.ts +160 -0
  122. package/tests/append-system-prompt.test.ts +46 -0
  123. package/tests/artifact-language.test.ts +19 -213
  124. package/tests/ask-user-question.test.ts +44 -1
  125. package/tests/asset-installation-runtime.test.ts +5 -16
  126. package/tests/autonomous-guard.test.ts +69 -1
  127. package/tests/bounded-writer-admission.test.ts +95 -0
  128. package/tests/branch-pr-skill.test.ts +43 -0
  129. package/tests/card-style-policy.test.ts +55 -0
  130. package/tests/chained-pr-skill.test.ts +124 -0
  131. package/tests/child-context-files.test.ts +255 -0
  132. package/tests/child-safety.test.ts +82 -0
  133. package/tests/codemode-rendering.test.ts +491 -0
  134. package/tests/command-palette.test.ts +39 -3
  135. package/tests/delegated-key-learnings-contract.test.ts +0 -76
  136. package/tests/destructive-command-guard.test.ts +84 -0
  137. package/tests/devbinary/native-review-parity.devtest.ts +170 -2
  138. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  139. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  140. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  141. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  142. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  143. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  144. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  145. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  146. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  147. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  148. package/tests/generic-agent-tools.test.ts +54 -0
  149. package/tests/gentle-agents.test.ts +1047 -252
  150. package/tests/gentle-ai-binary.test.ts +3 -3
  151. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  152. package/tests/gentle-ai-installer.test.ts +68 -54
  153. package/tests/gentle-ai-renderer.test.ts +487 -8
  154. package/tests/gentle-ai.test.ts +288 -65
  155. package/tests/gentle-card-text.ts +2 -1
  156. package/tests/gentle-shell-bin.test.ts +651 -114
  157. package/tests/gentle-shell-launcher.test.ts +99 -52
  158. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  159. package/tests/gentle-shell.test.ts +3839 -187
  160. package/tests/gentle-stats.test.ts +152 -0
  161. package/tests/gentle-theme.test.ts +4 -1
  162. package/tests/gentle-todo.test.ts +80 -8
  163. package/tests/history-atomic-write.test.ts +57 -0
  164. package/tests/history-capture-policy.test.ts +102 -0
  165. package/tests/history-command-registration.test.ts +164 -0
  166. package/tests/history-dedupe-entries.test.ts +123 -0
  167. package/tests/history-delete-backfill.test.ts +190 -0
  168. package/tests/history-delete-confirm.test.ts +460 -0
  169. package/tests/history-dispatch.test.ts +180 -0
  170. package/tests/history-drain-hidden.test.ts +110 -0
  171. package/tests/history-drain-order.test.ts +98 -0
  172. package/tests/history-expanded-globals.test.ts +62 -0
  173. package/tests/history-gc.test.ts +832 -0
  174. package/tests/history-header-layout.test.ts +265 -0
  175. package/tests/history-hide-prompts.test.ts +275 -0
  176. package/tests/history-lazy-windowing.test.ts +508 -0
  177. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  178. package/tests/history-load-shared-history.test.ts +53 -0
  179. package/tests/history-max-results-cap.test.ts +76 -0
  180. package/tests/history-multi-reader.test.ts +203 -0
  181. package/tests/history-off-path.test.ts +170 -0
  182. package/tests/history-openflow-integration.test.ts +173 -0
  183. package/tests/history-overlay-margin.test.ts +326 -0
  184. package/tests/history-preview-layout.test.ts +93 -0
  185. package/tests/history-registry.test.ts +143 -0
  186. package/tests/history-scope-delete.test.ts +411 -0
  187. package/tests/history-search-caret-keys.test.ts +142 -0
  188. package/tests/history-seed-bootstrap.test.ts +170 -0
  189. package/tests/history-seed-regen.test.ts +129 -0
  190. package/tests/history-selector-windowing.test.ts +94 -0
  191. package/tests/history-session-scan-directory.test.ts +87 -0
  192. package/tests/history-session-scan-extract.test.ts +583 -0
  193. package/tests/history-session-writer.test.ts +351 -0
  194. package/tests/history-store-paths.test.ts +79 -0
  195. package/tests/history-tombstone-exact.test.ts +139 -0
  196. package/tests/history-wheel-mouse.test.ts +242 -0
  197. package/tests/inprocess-reviewer.test.ts +29 -19
  198. package/tests/issue-creation-skill.test.ts +61 -0
  199. package/tests/model-routing-authority.test.ts +16 -0
  200. package/tests/nan-provider.test.ts +471 -0
  201. package/tests/native-review-capability-contract.test.ts +7 -1
  202. package/tests/native-review-cli.test.ts +6 -120
  203. package/tests/native-review-parity-runtime.test.ts +100 -3
  204. package/tests/odd-integration.test.ts +67 -0
  205. package/tests/odd-phase-inference.test.ts +213 -0
  206. package/tests/odd-phase-loader.test.ts +253 -0
  207. package/tests/odd-phase.test.ts +307 -0
  208. package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
  209. package/tests/odd-routing-contract.test.ts +86 -35
  210. package/tests/orchestrator-budget.test.ts +14 -39
  211. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  212. package/tests/overlay-repaint.test.ts +74 -0
  213. package/tests/package-manifest.test.ts +252 -115
  214. package/tests/packed-runner-owned-path.test.ts +46 -0
  215. package/tests/persona-single-channel.test.ts +6 -6
  216. package/tests/provider-defect-handoff.test.ts +3 -11
  217. package/tests/quiet-bash-runtime.test.ts +76 -0
  218. package/tests/quiet-tool-rendering.test.ts +409 -184
  219. package/tests/rdd-aware-verification-contract.test.ts +76 -1
  220. package/tests/rdd-status-line.test.ts +9 -4
  221. package/tests/resume-hint-extension.test.ts +122 -0
  222. package/tests/review-agent-end-preflight.test.ts +176 -12
  223. package/tests/review-candidate-owner-retry.test.ts +22 -1
  224. package/tests/review-candidate-view.test.ts +298 -0
  225. package/tests/review-contract-prompt.test.ts +108 -43
  226. package/tests/review-controller-lock-status.test.ts +0 -1
  227. package/tests/review-controller-native-routing.test.ts +611 -5
  228. package/tests/review-controller-workspace-root.test.ts +163 -4
  229. package/tests/review-host-relay-routing.test.ts +338 -2
  230. package/tests/review-integration-v2-forward.test.ts +200 -0
  231. package/tests/review-ledger-contract.test.ts +10 -34
  232. package/tests/review-reminder-receipt.test.ts +47 -1
  233. package/tests/review-risk-assessment.test.ts +498 -6
  234. package/tests/review-sidebar-state.test.ts +402 -0
  235. package/tests/run-test-suite.test.ts +124 -0
  236. package/tests/runtime-harness.mjs +145 -786
  237. package/tests/runtime-metrics-children.test.ts +16 -23
  238. package/tests/selection-engine.test.ts +421 -0
  239. package/tests/session-messaging-grants.test.ts +255 -0
  240. package/tests/session-worktree-registry.test.ts +77 -0
  241. package/tests/shell-bar.test.ts +382 -1
  242. package/tests/shell-card.test.ts +353 -1
  243. package/tests/shell-changes-view.test.ts +52 -0
  244. package/tests/shell-prompt.test.ts +94 -2
  245. package/tests/shell-sidebar-layout.test.ts +325 -21
  246. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  247. package/tests/shell-todo.test.ts +87 -1
  248. package/tests/shell-usage-view.test.ts +27 -0
  249. package/tests/shell-usage.test.ts +73 -0
  250. package/tests/skill-registry.test.ts +50 -1
  251. package/tests/startup-banner.test.ts +130 -2
  252. package/tests/stats-collector.test.ts +195 -0
  253. package/tests/stats-view.test.ts +202 -0
  254. package/tests/telemetry-trigger.test.ts +81 -20
  255. package/tests/theme-customization.test.ts +72 -0
  256. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  257. package/tests/vim-editor-adapter.test.ts +804 -0
  258. package/tests/vim-normal-engine.test.ts +101 -0
  259. package/tests/vim-operator-engine.test.ts +215 -0
  260. package/tests/vim-policy.test.ts +19 -0
  261. package/tests/vim-visual-engine.test.ts +52 -0
  262. package/tests/visual-customization-policy.test.ts +110 -0
  263. package/tests/visual-customize-view.test.ts +418 -0
  264. package/tests/visual-profiles.test.ts +87 -0
  265. package/tests/yolo-customize.test.ts +256 -0
  266. package/tests/yolo-mode-runtime.test.ts +161 -0
  267. package/tests/yolo-mode.test.ts +261 -0
  268. package/tests/yolo-session-policy.test.ts +59 -0
  269. package/themes/Gentle.json +2 -1
  270. package/themes/Gentleman-Cute.json +2 -1
  271. package/themes/Gentleman-Sexy.json +2 -1
  272. package/assets/agents/sdd-apply.md +0 -159
  273. package/assets/agents/sdd-archive.md +0 -228
  274. package/assets/agents/sdd-design.md +0 -49
  275. package/assets/agents/sdd-explore.md +0 -48
  276. package/assets/agents/sdd-init.md +0 -56
  277. package/assets/agents/sdd-onboard.md +0 -52
  278. package/assets/agents/sdd-proposal.md +0 -64
  279. package/assets/agents/sdd-remediate.md +0 -37
  280. package/assets/agents/sdd-research.md +0 -49
  281. package/assets/agents/sdd-spec.md +0 -192
  282. package/assets/agents/sdd-status.md +0 -54
  283. package/assets/agents/sdd-tasks.md +0 -108
  284. package/assets/agents/sdd-verify.md +0 -124
  285. package/assets/chains/sdd-full.chain.md +0 -83
  286. package/assets/chains/sdd-plan.chain.md +0 -56
  287. package/assets/chains/sdd-verify.chain.md +0 -43
  288. package/assets/sdd-orchestrator-workflow.md +0 -319
  289. package/assets/support/sdd-status-contract.md +0 -77
  290. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  291. package/extensions/sdd-init.ts +0 -816
  292. package/lib/openspec-deltas.ts +0 -156
  293. package/lib/sdd-preflight.ts +0 -1066
  294. package/lib/sdd-research-capabilities.ts +0 -94
  295. package/lib/sdd-status.ts +0 -26
  296. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  297. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  298. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  299. package/tests/openspec-deltas.test.ts +0 -209
  300. package/tests/sdd-agent-tools.test.ts +0 -156
  301. package/tests/sdd-archive-replay.test.ts +0 -82
  302. package/tests/sdd-classical-continuation.test.ts +0 -74
  303. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  304. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  305. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  306. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  307. package/tests/sdd-odd-integration.test.ts +0 -33
  308. package/tests/sdd-optional-research.test.ts +0 -124
  309. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  310. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  311. package/tests/sdd-preflight.test.ts +0 -541
  312. package/tests/sdd-research-capabilities.test.ts +0 -114
  313. package/tests/sdd-research-live.test.ts +0 -241
  314. package/tests/sdd-selection-transport.test.ts +0 -653
  315. package/tests/sdd-status.test.ts +0 -9
  316. package/tests/sdd-task-truth.test.ts +0 -43
@@ -2,6 +2,8 @@ import assert from "node:assert/strict";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import test from "node:test";
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
5
7
 
6
8
  // ---------------------------------------------------------------------------
7
9
  // gentle-pi#661/#662: RDD-aware verification rule for delegated work.
@@ -130,10 +132,11 @@ test("delegation overlay keeps the required headings", () => {
130
132
  "### Delegation Rules",
131
133
  "#### Background Subagent Policy",
132
134
  "#### Allowed edit surfaces (MANDATORY)",
133
- "### 3. SDD (optional)",
135
+ "### 2. Simple Delegation",
134
136
  ]) {
135
137
  assert.ok(delegation.includes(heading), `delegation overlay lost required heading: ${heading}`);
136
138
  }
139
+ assert.doesNotMatch(delegation, /### 3\. SDD \(optional\)/);
137
140
  });
138
141
 
139
142
  test("worker asset declares the Verification section after Test discipline", () => {
@@ -224,3 +227,75 @@ test("worker asset keeps the existing Return contract fields", () => {
224
227
  assert.ok(worker.includes(field), `worker asset lost Return contract field: ${field}`);
225
228
  }
226
229
  });
230
+
231
+ // ---------------------------------------------------------------------------
232
+ // gentle-pi#1175: the verification policy is stated on three surfaces -- the
233
+ // `gentle_review` tool contract (description, guidelines, parameters), the
234
+ // delegation asset, and docs/delegated-verification.md -- and they must say
235
+ // the same thing about the behavior ASSESS implements: closure only from the
236
+ // native `candidate.consumed` fact for this candidate, the runtime writer
237
+ // profile with a caller fallback, the conservative small-model bias, the
238
+ // declined/unavailable/unknown fallback, and functional checks that a native
239
+ // review never replaces. Assertions use short stable phrases, normalized for
240
+ // case, quoting, and line wrapping, not whole paragraphs.
241
+ // ---------------------------------------------------------------------------
242
+
243
+ interface RegisteredPolicyTool {
244
+ name: string;
245
+ description?: string;
246
+ promptSnippet?: string;
247
+ promptGuidelines?: readonly string[];
248
+ parameters?: unknown;
249
+ }
250
+
251
+ function registeredReviewToolContract(): string {
252
+ const tools = new Map<string, RegisteredPolicyTool>();
253
+ const pi = {
254
+ on() {},
255
+ registerCommand() {},
256
+ registerTool(definition: RegisteredPolicyTool) {
257
+ tools.set(definition.name, definition);
258
+ },
259
+ } as unknown as ExtensionAPI;
260
+ createGentleAiExtension({ nativeReviewCli: null })(pi);
261
+ const tool = tools.get("gentle_review");
262
+ assert.ok(tool, "gentle_review must be registered");
263
+ return [tool.description ?? "", tool.promptSnippet ?? "", ...(tool.promptGuidelines ?? []), JSON.stringify(tool.parameters)].join("\n");
264
+ }
265
+
266
+ function normalizePolicyText(text: string): string {
267
+ return text.replace(/\\"/g, "").replace(/[`"']/g, "").replace(/\s+/g, " ").toLowerCase();
268
+ }
269
+
270
+ const POLICY_SURFACES: ReadonlyArray<readonly [string, string]> = [
271
+ ["gentle_review tool contract", normalizePolicyText(registeredReviewToolContract())],
272
+ ["assets/orchestrator-delegation.md", normalizePolicyText(delegation)],
273
+ ["docs/delegated-verification.md", normalizePolicyText(read("docs/delegated-verification.md"))],
274
+ ];
275
+
276
+ const SHARED_POLICY_PHRASES: ReadonlyArray<readonly [string, string]> = [
277
+ ["closure only from native consumed evidence", "only from the native candidate.consumed fact for this exact candidate"],
278
+ ["caller closure claim is not authority", "a caller-supplied closed is not authority"],
279
+ ["runtime writer profile", "runtime-recorded model and effort of the pending mutations"],
280
+ ["caller profile is only a fallback", "only a fallback when no runtime evidence exists"],
281
+ ["conservative small-model bias", "keeps the conservative small-model bias"],
282
+ ["non-closed outcomes fall back", "declined, unavailable, or unknown"],
283
+ ["unknown is never closed", "unknown is never treated as closed"],
284
+ ["functional checks still run", "a native code review is not a substitute for applicable functional checks"],
285
+ ];
286
+
287
+ test("the three verification policy surfaces carry the same ASSESS closure, profile, fallback, and functional-check statements (gentle-pi#1175)", () => {
288
+ for (const [surface, text] of POLICY_SURFACES) {
289
+ for (const [statement, phrase] of SHARED_POLICY_PHRASES) {
290
+ assert.ok(text.includes(phrase), `${surface} is missing the ${statement} statement: "${phrase}"`);
291
+ }
292
+ }
293
+ });
294
+
295
+ test("no verification policy surface still tells callers to pass closed explicitly (gentle-pi#1175)", () => {
296
+ for (const [surface, text] of POLICY_SURFACES) {
297
+ for (const stale of ["closed is never derived", "closed is never auto-derived", "closed is never inferred", "pass it explicitly", "pass it only right after"]) {
298
+ assert.ok(!text.includes(stale), `${surface} still carries the retired explicit-closure wording: "${stale}"`);
299
+ }
300
+ }
301
+ });
@@ -176,10 +176,15 @@ test("resolveRddModeStatus resolves to undefined within the deadline when review
176
176
  const neverSettling = fakeReviewMode(() => new Promise<never>(() => {}));
177
177
  const deadlineMs = 150;
178
178
  const start = Date.now();
179
- const status = await resolveRddModeStatus(neverSettling, "/repo-hung", AbortSignal.timeout(deadlineMs));
180
- const elapsed = Date.now() - start;
181
- assert.equal(status, undefined);
182
- assert.ok(elapsed < deadlineMs + 1000, `expected the read to resolve near the ${deadlineMs}ms deadline, took ${elapsed}ms`);
179
+ const keepalive = setTimeout(() => {}, deadlineMs + 2000);
180
+ try {
181
+ const status = await resolveRddModeStatus(neverSettling, "/repo-hung", AbortSignal.timeout(deadlineMs));
182
+ const elapsed = Date.now() - start;
183
+ assert.equal(status, undefined);
184
+ assert.ok(elapsed < deadlineMs + 1000, `expected the read to resolve near the ${deadlineMs}ms deadline, took ${elapsed}ms`);
185
+ } finally {
186
+ clearTimeout(keepalive);
187
+ }
183
188
  });
184
189
 
185
190
  test("resolveRddModeStatus memoizes a resolved status per cwd for RDD_STATUS_MEMO_TTL_MS", async () => {
@@ -0,0 +1,122 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import test from "node:test";
6
+ import resumeHint, { resetResumeHintState } from "../extensions/resume-hint.ts";
7
+ import { RESUME_HANDOFF_ENV, parseResumeHandoff } from "../lib/gentle-shell-resume-hint.ts";
8
+
9
+ const ID = "01a0e0a0-6d7b-7314-89c1-537d47bbf4f3";
10
+
11
+ // A private dir shaped like the launcher's (<tmpdir>/gentle-shell-resume-XXXXXX).
12
+ const handoffDir = () => mkdtempSync(join(tmpdir(), "gentle-shell-resume-"));
13
+
14
+ type Handler = (event: { reason: string }, ctx: unknown) => void;
15
+
16
+ function loadExtension(env: NodeJS.ProcessEnv): Handler[] {
17
+ const shutdown: Handler[] = [];
18
+ const pi = { on: (name: string, handler: Handler) => { if (name === "session_shutdown") shutdown.push(handler); } };
19
+ resumeHint(pi as never, env);
20
+ return shutdown;
21
+ }
22
+
23
+ // The session cwd defaults to the launch directory (pi never changes it).
24
+ function fakeContext(dir: string, mode = "tui", cwd = process.cwd()) {
25
+ const sessionFile = join(dir, "session.jsonl");
26
+ writeFileSync(sessionFile, "{}\n");
27
+ return {
28
+ mode,
29
+ sessionManager: {
30
+ getSessionId: () => ID,
31
+ getSessionDir: () => dir,
32
+ getSessionFile: () => sessionFile,
33
+ getCwd: () => cwd,
34
+ },
35
+ };
36
+ }
37
+
38
+ test("extension is inert without the launcher handoff env", () => {
39
+ assert.equal(loadExtension({}).length, 0);
40
+ });
41
+
42
+ test("extension claims the env var so child processes cannot inherit it", () => {
43
+ const env: NodeJS.ProcessEnv = { [RESUME_HANDOFF_ENV]: join(tmpdir(), "gentle-shell-resume-unused", "handoff.json") };
44
+ loadExtension(env);
45
+ resetResumeHintState();
46
+ assert.equal(env[RESUME_HANDOFF_ENV], undefined);
47
+ });
48
+
49
+ test("extension keeps the claimed handoff across /reload", () => {
50
+ const dir = handoffDir();
51
+ try {
52
+ const handoffPath = join(dir, "handoff.json");
53
+ const env: NodeJS.ProcessEnv = { [RESUME_HANDOFF_ENV]: handoffPath };
54
+ loadExtension(env);
55
+ // A reload re-runs the factory with the env var already claimed.
56
+ const [onShutdown] = loadExtension(env);
57
+ assert.ok(onShutdown);
58
+ onShutdown({ reason: "quit" }, fakeContext(dir));
59
+ assert.equal(parseResumeHandoff(readFileSync(handoffPath, "utf8"))?.sessionId, ID);
60
+ } finally {
61
+ resetResumeHintState();
62
+ rmSync(dir, { recursive: true, force: true });
63
+ }
64
+ });
65
+
66
+ test("extension writes the handoff only for an interactive quit", () => {
67
+ const dir = handoffDir();
68
+ try {
69
+ const handoffPath = join(dir, "handoff.json");
70
+ const [onShutdown] = loadExtension({ [RESUME_HANDOFF_ENV]: handoffPath });
71
+ for (const reason of ["reload", "new", "resume", "fork"]) onShutdown({ reason }, fakeContext(dir));
72
+ for (const mode of ["rpc", "print", "json"]) onShutdown({ reason: "quit" }, fakeContext(dir, mode));
73
+ assert.throws(() => readFileSync(handoffPath, "utf8"));
74
+ onShutdown({ reason: "quit" }, fakeContext(dir));
75
+ assert.deepEqual(parseResumeHandoff(readFileSync(handoffPath, "utf8")), { sessionId: ID, sessionDir: dir });
76
+ } finally {
77
+ resetResumeHintState();
78
+ rmSync(dir, { recursive: true, force: true });
79
+ }
80
+ });
81
+
82
+ test("extension ignores a handoff path that is not the launcher's private file", () => {
83
+ const dir = handoffDir();
84
+ try {
85
+ const target = join(dir, "not-a-handoff.json");
86
+ const env: NodeJS.ProcessEnv = { [RESUME_HANDOFF_ENV]: target };
87
+ assert.equal(loadExtension(env).length, 0);
88
+ assert.equal(env[RESUME_HANDOFF_ENV], undefined);
89
+ } finally {
90
+ resetResumeHintState();
91
+ rmSync(dir, { recursive: true, force: true });
92
+ }
93
+ });
94
+
95
+ test("extension never overwrites an existing file or follows a planted symlink", { skip: process.platform === "win32" && "creating symlinks needs privileges on Windows" }, () => {
96
+ const dir = handoffDir();
97
+ try {
98
+ const victim = join(dir, "victim.txt");
99
+ writeFileSync(victim, "keep");
100
+ const handoffPath = join(dir, "handoff.json");
101
+ symlinkSync(victim, handoffPath);
102
+ const [onShutdown] = loadExtension({ [RESUME_HANDOFF_ENV]: handoffPath });
103
+ onShutdown({ reason: "quit" }, fakeContext(dir));
104
+ assert.equal(readFileSync(victim, "utf8"), "keep");
105
+ } finally {
106
+ resetResumeHintState();
107
+ rmSync(dir, { recursive: true, force: true });
108
+ }
109
+ });
110
+
111
+ test("extension hands off the session file for a session from another project", () => {
112
+ const dir = handoffDir();
113
+ try {
114
+ const handoffPath = join(dir, "handoff.json");
115
+ const [onShutdown] = loadExtension({ [RESUME_HANDOFF_ENV]: handoffPath });
116
+ onShutdown({ reason: "quit" }, fakeContext(dir, "tui", resolve("/home/u/other-project")));
117
+ assert.deepEqual(parseResumeHandoff(readFileSync(handoffPath, "utf8")), { sessionId: ID, sessionFile: join(dir, "session.jsonl") });
118
+ } finally {
119
+ resetResumeHintState();
120
+ rmSync(dir, { recursive: true, force: true });
121
+ }
122
+ });
@@ -1,12 +1,12 @@
1
1
  import assert from "node:assert/strict";
2
2
  import childProcess from "node:child_process";
3
3
  import { syncBuiltinESMExports } from "node:module";
4
- import { realpathSync } from "node:fs";
5
- import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
4
+ import { existsSync, realpathSync } from "node:fs";
5
+ import { mkdir, mkdtemp, rename, writeFile, rm } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
- import test from "node:test";
9
- import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
8
+ import test, { after, before } from "node:test";
9
+ import type { ExtensionAPI, ExtensionContext, ExtensionToolContext } from "@earendil-works/pi-coding-agent";
10
10
  import { createGentleAiExtension } from "../extensions/gentle-ai.ts";
11
11
  import { NATIVE_REVIEW_ERROR_CODE, NativeReviewCliError, type NativeReviewCli } from "../lib/native-review-cli.ts";
12
12
  import type { ReviewStatusV3 } from "../lib/review-integration-v2.ts";
@@ -29,8 +29,41 @@ type AnyHandler = (event: unknown, ctx: ExtensionContext) => unknown;
29
29
  type RegisteredTool = Parameters<ExtensionAPI["registerTool"]>[0];
30
30
  type SentMessage = { message: Record<string, unknown>; options: Record<string, unknown> };
31
31
 
32
+ let fixtureCwd: string;
33
+ let fixtureRoot: string | undefined;
34
+ const fixtureEnvironment: NodeJS.ProcessEnv = {};
35
+ const previousEnvironment = new Map<string, string | undefined>();
36
+ before(async () => {
37
+ const root = await mkdtemp(join(tmpdir(), "gentle-pi-preflight-"));
38
+ fixtureRoot = root;
39
+ fixtureCwd = join(root, "project");
40
+ const home = join(root, "home");
41
+ for (const directory of [fixtureCwd, home, join(fixtureCwd, "src"), join(fixtureCwd, "tests")]) await mkdir(directory);
42
+ await writeFile(join(fixtureCwd, "src/example.ts"), "export const value = 1;");
43
+ childProcess.execFileSync("git", ["init", "--quiet", fixtureCwd]);
44
+ fixtureCwd = realpathSync(fixtureCwd);
45
+ Object.assign(fixtureEnvironment, {
46
+ HOME: home, USERPROFILE: home,
47
+ GENTLE_PI_CONFIG_HOME: join(home, "config"),
48
+ GENTLE_PI_AGENT_HOME: join(home, "agents"),
49
+ PI_CODING_AGENT_DIR: join(home, "pi"),
50
+ XDG_CONFIG_HOME: join(home, "xdg"),
51
+ });
52
+ for (const [key, value] of Object.entries(fixtureEnvironment)) {
53
+ previousEnvironment.set(key, process.env[key]);
54
+ process.env[key] = value;
55
+ }
56
+ });
57
+ after(async () => {
58
+ for (const [key, value] of previousEnvironment) {
59
+ if (value === undefined) delete process.env[key];
60
+ else process.env[key] = value;
61
+ }
62
+ if (fixtureRoot !== undefined) await rm(fixtureRoot, { recursive: true, force: true });
63
+ });
64
+
32
65
  type CustomEntry = { type: string; customType: string; data: unknown };
33
- function harness(nativeReviewCli: NativeReviewCli | null, entries: CustomEntry[] = []): {
66
+ function harness(nativeReviewCli: NativeReviewCli | null, entries: CustomEntry[] = [], processEnv?: NodeJS.ProcessEnv): {
34
67
  handlers: Map<string, AnyHandler>;
35
68
  sent: SentMessage[];
36
69
  tools: Map<string, RegisteredTool>;
@@ -53,11 +86,16 @@ function harness(nativeReviewCli: NativeReviewCli | null, entries: CustomEntry[]
53
86
  sent.push({ message, options });
54
87
  },
55
88
  } as unknown as ExtensionAPI;
56
- createGentleAiExtension({ nativeReviewCli })(pi);
89
+ createGentleAiExtension({
90
+ nativeReviewCli,
91
+ processEnv: { ...fixtureEnvironment, GENTLE_PI_AGENTS_CHILD: "0", ...processEnv, GENTLE_AI_TELEMETRY: "0" },
92
+ resolveTelemetryTriggerBinary: () => join(fixtureCwd, "never-executed"),
93
+ telemetryTriggerSpawn: () => assert.fail("Preflight fixtures must not spawn telemetry"),
94
+ })(pi);
57
95
  return { handlers, sent, tools };
58
96
  }
59
97
 
60
- function ctx(sessionId: string, hasUI = true, cwd = process.cwd()): ExtensionContext {
98
+ function ctx(sessionId: string, hasUI = true, cwd = fixtureCwd): ExtensionContext {
61
99
  return {
62
100
  cwd,
63
101
  hasUI,
@@ -66,7 +104,17 @@ function ctx(sessionId: string, hasUI = true, cwd = process.cwd()): ExtensionCon
66
104
  } as unknown as ExtensionContext;
67
105
  }
68
106
 
69
- async function withSessionStartEnv<T>(callback: (cwd: string) => Promise<T>): Promise<T> {
107
+ function toolContext(context: ExtensionContext): ExtensionToolContext {
108
+ return {
109
+ ...context,
110
+ tools: [],
111
+ async executeTool() {
112
+ assert.fail("Review preflight must not execute nested tools");
113
+ },
114
+ };
115
+ }
116
+
117
+ async function withSessionStartEnv<T>(callback: (cwd: string) => Promise<T>, initializeGit = true): Promise<T> {
70
118
  const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
71
119
  const previousConfigHome = process.env.GENTLE_PI_CONFIG_HOME;
72
120
  process.env.GENTLE_PI_AGENT_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-session-baseline-agent-home-"));
@@ -76,7 +124,7 @@ async function withSessionStartEnv<T>(callback: (cwd: string) => Promise<T>): Pr
76
124
  process.env.GENTLE_PI_CONFIG_HOME = await mkdtemp(join(tmpdir(), "gentle-pi-session-baseline-config-home-"));
77
125
  try {
78
126
  const cwd = await mkdtemp(join(tmpdir(), "gentle-pi-session-baseline-cwd-"));
79
- childProcess.execFileSync("git", ["init", "--quiet", cwd]);
127
+ if (initializeGit) childProcess.execFileSync("git", ["init", "--quiet", cwd]);
80
128
  await mkdir(join(cwd, "src"));
81
129
  await writeFile(join(cwd, "src/example.ts"), "export const value = 1;");
82
130
  return await callback(cwd);
@@ -127,7 +175,7 @@ const agentEndEvent = { type: "agent_end", messages: [] };
127
175
  let mutationCall = 0;
128
176
  async function directWrite(handlers: Map<string, AnyHandler>, session: ExtensionContext): Promise<void> {
129
177
  await handlers.get("tool_result")!({ type: "tool_result", toolName: "write", toolCallId: `own-${++mutationCall}`,
130
- input: { path: session.cwd === process.cwd() || session.cwd === realpathSync(process.cwd()) ? "tests/review-agent-end-preflight.test.ts" : "src/example.ts" },
178
+ input: { path: "src/example.ts" },
131
179
  content: [], isError: false }, session);
132
180
  }
133
181
 
@@ -138,7 +186,7 @@ for (const scenario of ["same", "changed", "sibling-root", "nested-root", "faile
138
186
  test(`agent_end after approved acknowledgement: ${scenario}`, async (t) => {
139
187
  const changedTarget = scenario === "changed";
140
188
  const unsuccessful = scenario === "failed" || scenario === "unknown";
141
- const cwd = realpathSync(process.cwd());
189
+ const cwd = fixtureCwd;
142
190
  const siblingRoot = realpathSync(tmpdir());
143
191
  if (scenario === "sibling-root") {
144
192
  // Model a sibling worktree sharing the real common directory without
@@ -210,7 +258,7 @@ for (const scenario of ["same", "changed", "sibling-root", "nested-root", "faile
210
258
  const review = tools.get("gentle_review");
211
259
  assert.ok(review);
212
260
  await directWrite(handlers, session);
213
- const result = await review.execute("post-ack", { operation: "acknowledge-approved", lineageId }, undefined, undefined, session);
261
+ const result = await review.execute("post-ack", { operation: "acknowledge-approved", lineageId }, undefined, undefined, toolContext(session));
214
262
  if (unsuccessful) {
215
263
  assert.equal(result.details.outcome, scenario === "failed" ? "native-operation-failed" : "native-mutation-status-reconciled");
216
264
  assert.equal(result.details.mutation_outcome, scenario === "failed" ? "none" : "unknown");
@@ -242,6 +290,22 @@ for (const scenario of ["same", "changed", "sibling-root", "nested-root", "faile
242
290
  });
243
291
  }
244
292
 
293
+ test("passive session events outside Git with RDD off never invoke native STATUS or initialize Git", async () => {
294
+ await withSessionStartEnv(async (cwd) => {
295
+ const calls: string[] = [];
296
+ const native = {
297
+ reviewMode: async () => { calls.push("reviewMode"); return onMode("off")!({} as never); },
298
+ targetStatus: async () => { calls.push("targetStatus"); return executeStartStatus("outside-git"); },
299
+ } as unknown as NativeReviewCli;
300
+ const { handlers } = harness(native);
301
+ const session = ctx("outside-git", true, cwd);
302
+ await handlers.get("session_start")!({ type: "session_start" }, session);
303
+ await handlers.get("agent_end")!(agentEndEvent, session);
304
+ assert.deepEqual(calls, [], "passive non-Git events need no native entry");
305
+ assert.equal(existsSync(join(cwd, ".git")), false, "passive events must not initialize Git");
306
+ }, false);
307
+ });
308
+
245
309
  test("agent_end performs no STATUS call and sends nothing when RDD is off", async () => {
246
310
  const statusRequests: unknown[] = [];
247
311
  const native = {
@@ -383,6 +447,25 @@ test("gentle-ai-worker agent_end never queries native review", async () => {
383
447
  assert.deepEqual(sent, []);
384
448
  });
385
449
 
450
+ test("agent_end sends nothing and performs no STATUS call when GENTLE_PI_AGENTS_CHILD is 1", async () => {
451
+ const statusRequests: unknown[] = [];
452
+ const native = {
453
+ reviewMode: onMode("on"),
454
+ targetStatus: async (request: unknown) => {
455
+ statusRequests.push(request);
456
+ throw new Error("targetStatus must not be called in a child session");
457
+ },
458
+ } as unknown as NativeReviewCli;
459
+ const { handlers, sent } = harness(native, [], { GENTLE_PI_AGENTS_CHILD: "1" });
460
+ const agentEnd = handlers.get("agent_end");
461
+ assert.equal(typeof agentEnd, "function");
462
+ const session = ctx("agent-end-child");
463
+ await directWrite(handlers, session);
464
+ await agentEnd!(agentEndEvent, session);
465
+ assert.deepEqual(statusRequests, []);
466
+ assert.deepEqual(sent, []);
467
+ });
468
+
386
469
  test("agent_end resets the subagent depth when a fresh primary loop starts", async () => {
387
470
  const targetIdentity = `sha256:${"1".repeat(64)}`;
388
471
  const native = {
@@ -520,6 +603,87 @@ for (const toolName of ["read", "bash", "subagent_run", "edit", "write"]) {
520
603
  });
521
604
  }
522
605
 
606
+ for (const mode of ["on", "off", "unknown", "missing", "malformed", "incompatible", "throws"] as const) {
607
+ test(`non-Git successful source write, never startup, prepares only with validated RDD on: ${mode}`, async () => {
608
+ await withSessionStartEnv(async (cwd) => {
609
+ const requests: unknown[] = [];
610
+ const native = {
611
+ reviewMode: mode === "missing" ? undefined : async () => {
612
+ if (mode === "throws") throw new Error("unavailable");
613
+ if (mode === "incompatible") throw new NativeReviewCliError(NATIVE_REVIEW_ERROR_CODE.VERSION_INCOMPATIBLE, "review/mode", false, false, "incompatible");
614
+ if (mode === "malformed") return { status: { effective: "on", source: "unrecognized" } };
615
+ return { operation: "status", scope: "clone", status: { global: "", cloneLocal: "", effective: mode, source: "global" } };
616
+ },
617
+ targetStatus: async (request: unknown) => {
618
+ requests.push(request);
619
+ // The fake native owns bootstrap, not the extension.
620
+ childProcess.execFileSync("git", ["init", "--quiet", cwd]);
621
+ return executeStartStatus(`sha256:${"8".repeat(64)}`);
622
+ },
623
+ } as unknown as NativeReviewCli;
624
+ const { handlers, sent } = harness(native);
625
+ const session = ctx(`non-git-${mode}`, true, cwd);
626
+ await handlers.get("session_start")!({}, session);
627
+ assert.equal(requests.length, 0, "passive startup cannot bootstrap");
628
+ assert.equal(existsSync(join(cwd, ".git")), false);
629
+ await handlers.get("agent_end")!(agentEndEvent, session);
630
+ assert.equal(requests.length, 0, "conversation cannot bootstrap");
631
+ await directWrite(handlers, session);
632
+ assert.equal(requests.length, mode === "on" ? 1 : 0);
633
+ assert.equal(existsSync(join(cwd, ".git")), mode === "on");
634
+ assert.deepEqual(sent, [], "preparation alone does not remind");
635
+ }, false);
636
+ });
637
+ }
638
+
639
+ for (const path of ["odd/tasks/feature.md", "README.md", ".pi/settings.json", "memory.md", "secrets/app.ts", "../foreign.ts"]) {
640
+ test(`bookkeeping or unsafe write cannot prepare: ${path}`, async () => {
641
+ await withSessionStartEnv(async (cwd) => {
642
+ let calls = 0;
643
+ const h = harness({ reviewMode: onMode("on"), targetStatus: async () => { calls++; return stopStatus("unused"); } } as unknown as NativeReviewCli);
644
+ const session = ctx("non-source", false, cwd);
645
+ await h.handlers.get("session_start")!({}, session);
646
+ await h.handlers.get("tool_result")!({ toolName: "write", toolCallId: "bookkeeping", input: { path }, isError: false }, session);
647
+ assert.equal(calls, 0);
648
+ assert.equal(existsSync(join(cwd, ".git")), false);
649
+ }, false);
650
+ });
651
+ }
652
+
653
+ for (const initiallyGit of [true, false]) {
654
+ test(`source preparation never reinitializes established metadata after loss: initially Git=${initiallyGit}`, async () => {
655
+ await withSessionStartEnv(async cwd => {
656
+ let calls = 0;
657
+ const h = harness({ reviewMode: onMode("on"), targetStatus: async () => {
658
+ calls++; childProcess.execFileSync("git", ["init", "--quiet", cwd]); return stopStatus("prepared");
659
+ } } as unknown as NativeReviewCli);
660
+ const session = ctx("established-source", false, cwd);
661
+ await h.handlers.get("session_start")!({}, session);
662
+ if (!initiallyGit) { await directWrite(h.handlers, session); assert.equal(calls, 1); }
663
+ await rename(join(cwd, ".git"), join(cwd, "saved-git"));
664
+ calls = 0;
665
+ await directWrite(h.handlers, session);
666
+ assert.equal(calls, 0, "no native bootstrap STATUS after previously established Git disappears");
667
+ assert.equal(existsSync(join(cwd, ".git")), false, "lost metadata remains lost, not reinitialized");
668
+ }, initiallyGit);
669
+ });
670
+ }
671
+
672
+ test("headless successful source write prepares after binding without a reminder", async () => {
673
+ await withSessionStartEnv(async cwd => {
674
+ let calls = 0;
675
+ const h = harness({ reviewMode: onMode("on"), targetStatus: async () => {
676
+ calls++; childProcess.execFileSync("git", ["init", "--quiet", cwd]); return stopStatus("prepared");
677
+ } } as unknown as NativeReviewCli);
678
+ const session = ctx("headless-source", false, cwd);
679
+ await h.handlers.get("session_start")!({}, session);
680
+ await directWrite(h.handlers, session);
681
+ assert.equal(calls, 1);
682
+ assert.equal(existsSync(join(cwd, ".git")), true);
683
+ assert.deepEqual(h.sent, []);
684
+ }, false);
685
+ });
686
+
523
687
  test("session_start negotiates the current target identity when RDD is on", async () => {
524
688
  const targetIdentity = `sha256:${"2".repeat(64)}`;
525
689
  const statusRequests: Array<{ agent?: string }> = [];
@@ -1,6 +1,6 @@
1
1
  import assert from "node:assert/strict";
2
2
  import { test } from "node:test";
3
- import { withWindowsSystemProbeRetry } from "../lib/review-candidate-view-owner.ts";
3
+ import { resolveWindowsSystemProbeTimeoutMs, withWindowsSystemProbeRetry } from "../lib/review-candidate-view-owner.ts";
4
4
 
5
5
  function failingProbe(code: string, attempts: { count: number }): () => string {
6
6
  return () => {
@@ -61,3 +61,24 @@ test("windows system probe does not retry when the first attempt succeeds", () =
61
61
  assert.equal(value, "S-1-5-18");
62
62
  assert.equal(attempts.count, 1);
63
63
  });
64
+
65
+ test("windows system probes default to a cold-runner-safe bounded timeout", () => {
66
+ assert.equal(resolveWindowsSystemProbeTimeoutMs({}), 15_000);
67
+ });
68
+
69
+ test("windows system probe timeout honors a bounded environment override", () => {
70
+ const env = (value: string) => ({ GENTLE_PI_CANDIDATE_WINDOWS_PROBE_TIMEOUT_MS: value });
71
+ assert.equal(resolveWindowsSystemProbeTimeoutMs(env("30000")), 30_000);
72
+ assert.equal(resolveWindowsSystemProbeTimeoutMs(env("120000")), 120_000);
73
+ for (const invalid of ["0", "-1", "abc", "1.5", "120001", "", "015000"]) {
74
+ assert.equal(resolveWindowsSystemProbeTimeoutMs(env(invalid)), 15_000, invalid);
75
+ }
76
+ });
77
+
78
+ test("every windows system probe uses the configurable timeout", async () => {
79
+ const { readFileSync } = await import("node:fs");
80
+ const source = readFileSync(new URL("../lib/review-candidate-view-owner.ts", import.meta.url), "utf8");
81
+ const probes = source.match(/windowsSystemExecutable\([^)]*\), \[[^\]]*\], \{[^}]*\}/g) ?? [];
82
+ assert.equal(probes.length, 5);
83
+ for (const probe of probes) assert.match(probe, /timeout: resolveWindowsSystemProbeTimeoutMs\(\)/);
84
+ });