gentle-pi 3.6.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 (320) 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 +95 -30
  15. package/docs/native-authority-architecture.md +2 -2
  16. package/docs/prompt-history.md +280 -0
  17. package/docs/readme-reference.md +117 -235
  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 +509 -275
  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 +105 -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/foreign-target-grants.ts +32 -0
  55. package/lib/gentle-ai-elapsed-store.ts +87 -0
  56. package/lib/gentle-ai-renderer.ts +196 -39
  57. package/lib/gentle-shell-launcher.ts +24 -13
  58. package/lib/gentle-shell-resume-hint.ts +176 -0
  59. package/lib/history-capture-policy.ts +95 -0
  60. package/lib/model-routing-authority.ts +5 -1
  61. package/lib/nan-provider.ts +227 -0
  62. package/lib/native-review-cli.ts +53 -108
  63. package/lib/odd-phase-inference.ts +231 -0
  64. package/lib/odd-phase.ts +141 -0
  65. package/lib/overlay-repaint.ts +26 -0
  66. package/lib/pi-tui-keys.ts +53 -0
  67. package/lib/review-candidate-view-owner.ts +67 -17
  68. package/lib/review-candidate-view.ts +112 -25
  69. package/lib/review-reminder-receipt.ts +48 -8
  70. package/lib/review-risk-assessment.ts +156 -11
  71. package/lib/review-sidebar-state.ts +223 -0
  72. package/lib/selection-engine.ts +515 -0
  73. package/lib/session-change-capture.ts +10 -1
  74. package/lib/session-messaging-grants.ts +135 -0
  75. package/lib/session-worktree-registry.ts +14 -2
  76. package/lib/shell-bar.ts +179 -24
  77. package/lib/shell-card.ts +287 -18
  78. package/lib/shell-changes-view.ts +2 -1
  79. package/lib/shell-prompt.ts +102 -6
  80. package/lib/shell-sidebar-layout.ts +78 -14
  81. package/lib/shell-sidebar.ts +15 -1
  82. package/lib/shell-todo.ts +23 -13
  83. package/lib/shell-usage-view.ts +9 -4
  84. package/lib/shell-usage.ts +66 -10
  85. package/lib/stats-collector.ts +381 -0
  86. package/lib/stats-view.ts +431 -0
  87. package/lib/theme-customization.ts +52 -0
  88. package/lib/vim-editor-adapter.ts +379 -0
  89. package/lib/vim-normal-engine.ts +154 -0
  90. package/lib/vim-operator-engine.ts +416 -0
  91. package/lib/vim-policy.ts +49 -0
  92. package/lib/vim-visual-engine.ts +107 -0
  93. package/lib/visual-customization-policy.ts +108 -0
  94. package/lib/visual-customize-view.ts +330 -0
  95. package/lib/visual-profiles.ts +228 -0
  96. package/lib/yolo-session-policy.ts +240 -0
  97. package/package.json +20 -8
  98. package/runtime/gentle-shell-launcher.mjs +23 -12
  99. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  100. package/runtime/native-review-cli.mjs +53 -108
  101. package/runtime/review-risk-assessment.mjs +154 -9
  102. package/scripts/build-runtime-modules.mjs +1 -0
  103. package/scripts/gentle-ai-installer.mjs +14 -13
  104. package/scripts/mirror-odd-routing.mjs +2 -2
  105. package/scripts/run-test-suite.mjs +76 -0
  106. package/scripts/test-packed-runner.mjs +31 -14
  107. package/scripts/verify-package-files.mjs +11 -22
  108. package/skills/branch-pr/SKILL.md +24 -52
  109. package/skills/chained-pr/SKILL.md +31 -15
  110. package/skills/chained-pr/references/chaining-details.md +31 -20
  111. package/skills/gentle-ai/SKILL.md +9 -15
  112. package/skills/issue-creation/SKILL.md +8 -2
  113. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  114. package/skills/work-unit-commits/SKILL.md +4 -3
  115. package/tests/agent-profiles.test.ts +18 -0
  116. package/tests/agents-fake-child.ts +2 -2
  117. package/tests/agents-message-delivery.test.ts +106 -0
  118. package/tests/agents-protocol.test.ts +40 -0
  119. package/tests/agents-runner.test.ts +400 -89
  120. package/tests/agents-view-thread-identity.test.ts +169 -0
  121. package/tests/agents-view.test.ts +8 -2
  122. package/tests/agents-widget.test.ts +154 -15
  123. package/tests/append-system-prompt-route.test.ts +160 -0
  124. package/tests/append-system-prompt.test.ts +46 -0
  125. package/tests/artifact-language.test.ts +19 -213
  126. package/tests/ask-user-question.test.ts +44 -1
  127. package/tests/asset-installation-runtime.test.ts +5 -16
  128. package/tests/autonomous-guard.test.ts +69 -1
  129. package/tests/bounded-writer-admission.test.ts +95 -0
  130. package/tests/branch-pr-skill.test.ts +43 -0
  131. package/tests/card-style-policy.test.ts +55 -0
  132. package/tests/chained-pr-skill.test.ts +124 -0
  133. package/tests/child-context-files.test.ts +255 -0
  134. package/tests/child-safety.test.ts +82 -0
  135. package/tests/codemode-rendering.test.ts +491 -0
  136. package/tests/command-palette.test.ts +39 -3
  137. package/tests/delegated-key-learnings-contract.test.ts +0 -76
  138. package/tests/destructive-command-guard.test.ts +84 -0
  139. package/tests/devbinary/native-review-parity.devtest.ts +170 -2
  140. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  141. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  142. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  143. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  144. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  145. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  146. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  147. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  148. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  149. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  150. package/tests/foreign-target-grants.test.ts +58 -0
  151. package/tests/generic-agent-tools.test.ts +54 -0
  152. package/tests/gentle-agents.test.ts +1411 -256
  153. package/tests/gentle-ai-binary.test.ts +3 -3
  154. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  155. package/tests/gentle-ai-installer.test.ts +68 -54
  156. package/tests/gentle-ai-renderer.test.ts +487 -8
  157. package/tests/gentle-ai.test.ts +288 -65
  158. package/tests/gentle-card-text.ts +2 -1
  159. package/tests/gentle-shell-bin.test.ts +651 -114
  160. package/tests/gentle-shell-launcher.test.ts +99 -52
  161. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  162. package/tests/gentle-shell.test.ts +3839 -187
  163. package/tests/gentle-stats.test.ts +152 -0
  164. package/tests/gentle-theme.test.ts +4 -1
  165. package/tests/gentle-todo.test.ts +80 -8
  166. package/tests/history-atomic-write.test.ts +57 -0
  167. package/tests/history-capture-policy.test.ts +102 -0
  168. package/tests/history-command-registration.test.ts +164 -0
  169. package/tests/history-dedupe-entries.test.ts +123 -0
  170. package/tests/history-delete-backfill.test.ts +190 -0
  171. package/tests/history-delete-confirm.test.ts +460 -0
  172. package/tests/history-dispatch.test.ts +180 -0
  173. package/tests/history-drain-hidden.test.ts +110 -0
  174. package/tests/history-drain-order.test.ts +98 -0
  175. package/tests/history-expanded-globals.test.ts +62 -0
  176. package/tests/history-gc.test.ts +832 -0
  177. package/tests/history-header-layout.test.ts +265 -0
  178. package/tests/history-hide-prompts.test.ts +275 -0
  179. package/tests/history-lazy-windowing.test.ts +508 -0
  180. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  181. package/tests/history-load-shared-history.test.ts +53 -0
  182. package/tests/history-max-results-cap.test.ts +76 -0
  183. package/tests/history-multi-reader.test.ts +203 -0
  184. package/tests/history-off-path.test.ts +170 -0
  185. package/tests/history-openflow-integration.test.ts +173 -0
  186. package/tests/history-overlay-margin.test.ts +326 -0
  187. package/tests/history-preview-layout.test.ts +93 -0
  188. package/tests/history-registry.test.ts +143 -0
  189. package/tests/history-scope-delete.test.ts +411 -0
  190. package/tests/history-search-caret-keys.test.ts +142 -0
  191. package/tests/history-seed-bootstrap.test.ts +170 -0
  192. package/tests/history-seed-regen.test.ts +129 -0
  193. package/tests/history-selector-windowing.test.ts +94 -0
  194. package/tests/history-session-scan-directory.test.ts +87 -0
  195. package/tests/history-session-scan-extract.test.ts +583 -0
  196. package/tests/history-session-writer.test.ts +351 -0
  197. package/tests/history-store-paths.test.ts +79 -0
  198. package/tests/history-tombstone-exact.test.ts +139 -0
  199. package/tests/history-wheel-mouse.test.ts +242 -0
  200. package/tests/inprocess-reviewer.test.ts +29 -19
  201. package/tests/issue-creation-skill.test.ts +61 -0
  202. package/tests/model-routing-authority.test.ts +16 -0
  203. package/tests/nan-provider.test.ts +471 -0
  204. package/tests/native-review-capability-contract.test.ts +13 -1
  205. package/tests/native-review-cli.test.ts +6 -120
  206. package/tests/native-review-parity-runtime.test.ts +100 -3
  207. package/tests/odd-integration.test.ts +67 -0
  208. package/tests/odd-phase-inference.test.ts +213 -0
  209. package/tests/odd-phase-loader.test.ts +253 -0
  210. package/tests/odd-phase.test.ts +307 -0
  211. package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
  212. package/tests/odd-routing-contract.test.ts +86 -35
  213. package/tests/orchestrator-budget.test.ts +14 -39
  214. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  215. package/tests/overlay-repaint.test.ts +74 -0
  216. package/tests/package-manifest.test.ts +252 -115
  217. package/tests/packed-runner-owned-path.test.ts +46 -0
  218. package/tests/persona-single-channel.test.ts +6 -6
  219. package/tests/provider-defect-handoff.test.ts +3 -11
  220. package/tests/quiet-bash-runtime.test.ts +76 -0
  221. package/tests/quiet-tool-rendering.test.ts +409 -184
  222. package/tests/rdd-aware-verification-contract.test.ts +76 -1
  223. package/tests/rdd-status-line.test.ts +9 -4
  224. package/tests/resume-hint-extension.test.ts +122 -0
  225. package/tests/review-agent-end-preflight.test.ts +176 -12
  226. package/tests/review-candidate-owner-retry.test.ts +22 -1
  227. package/tests/review-candidate-view.test.ts +298 -0
  228. package/tests/review-contract-prompt.test.ts +108 -43
  229. package/tests/review-controller-lock-status.test.ts +0 -1
  230. package/tests/review-controller-native-routing.test.ts +611 -5
  231. package/tests/review-controller-workspace-root.test.ts +163 -4
  232. package/tests/review-host-relay-routing.test.ts +338 -2
  233. package/tests/review-integration-v2-forward.test.ts +200 -0
  234. package/tests/review-ledger-contract.test.ts +10 -34
  235. package/tests/review-reminder-receipt.test.ts +47 -1
  236. package/tests/review-risk-assessment.test.ts +498 -6
  237. package/tests/review-sidebar-state.test.ts +402 -0
  238. package/tests/run-test-suite.test.ts +124 -0
  239. package/tests/runtime-harness.mjs +145 -786
  240. package/tests/runtime-metrics-children.test.ts +16 -23
  241. package/tests/selection-engine.test.ts +421 -0
  242. package/tests/session-change-capture.test.ts +12 -1
  243. package/tests/session-messaging-grants.test.ts +255 -0
  244. package/tests/session-worktree-registry.test.ts +77 -0
  245. package/tests/shell-bar.test.ts +382 -1
  246. package/tests/shell-card.test.ts +353 -1
  247. package/tests/shell-changes-view.test.ts +52 -0
  248. package/tests/shell-prompt.test.ts +94 -2
  249. package/tests/shell-sidebar-layout.test.ts +325 -21
  250. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  251. package/tests/shell-todo.test.ts +87 -1
  252. package/tests/shell-usage-view.test.ts +27 -0
  253. package/tests/shell-usage.test.ts +73 -0
  254. package/tests/skill-registry.test.ts +50 -1
  255. package/tests/startup-banner.test.ts +130 -2
  256. package/tests/stats-collector.test.ts +195 -0
  257. package/tests/stats-view.test.ts +202 -0
  258. package/tests/telemetry-trigger.test.ts +81 -20
  259. package/tests/theme-customization.test.ts +72 -0
  260. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  261. package/tests/vim-editor-adapter.test.ts +804 -0
  262. package/tests/vim-normal-engine.test.ts +101 -0
  263. package/tests/vim-operator-engine.test.ts +215 -0
  264. package/tests/vim-policy.test.ts +19 -0
  265. package/tests/vim-visual-engine.test.ts +52 -0
  266. package/tests/visual-customization-policy.test.ts +110 -0
  267. package/tests/visual-customize-view.test.ts +418 -0
  268. package/tests/visual-profiles.test.ts +87 -0
  269. package/tests/yolo-customize.test.ts +256 -0
  270. package/tests/yolo-mode-runtime.test.ts +161 -0
  271. package/tests/yolo-mode.test.ts +261 -0
  272. package/tests/yolo-session-policy.test.ts +59 -0
  273. package/themes/Gentle.json +2 -1
  274. package/themes/Gentleman-Cute.json +2 -1
  275. package/themes/Gentleman-Sexy.json +2 -1
  276. package/assets/agents/sdd-apply.md +0 -159
  277. package/assets/agents/sdd-archive.md +0 -228
  278. package/assets/agents/sdd-design.md +0 -49
  279. package/assets/agents/sdd-explore.md +0 -48
  280. package/assets/agents/sdd-init.md +0 -56
  281. package/assets/agents/sdd-onboard.md +0 -52
  282. package/assets/agents/sdd-proposal.md +0 -64
  283. package/assets/agents/sdd-remediate.md +0 -37
  284. package/assets/agents/sdd-research.md +0 -49
  285. package/assets/agents/sdd-spec.md +0 -192
  286. package/assets/agents/sdd-status.md +0 -54
  287. package/assets/agents/sdd-tasks.md +0 -108
  288. package/assets/agents/sdd-verify.md +0 -124
  289. package/assets/chains/sdd-full.chain.md +0 -83
  290. package/assets/chains/sdd-plan.chain.md +0 -56
  291. package/assets/chains/sdd-verify.chain.md +0 -43
  292. package/assets/sdd-orchestrator-workflow.md +0 -319
  293. package/assets/support/sdd-status-contract.md +0 -77
  294. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  295. package/extensions/sdd-init.ts +0 -816
  296. package/lib/openspec-deltas.ts +0 -156
  297. package/lib/sdd-preflight.ts +0 -1066
  298. package/lib/sdd-research-capabilities.ts +0 -94
  299. package/lib/sdd-status.ts +0 -26
  300. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  301. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  302. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  303. package/tests/openspec-deltas.test.ts +0 -209
  304. package/tests/sdd-agent-tools.test.ts +0 -156
  305. package/tests/sdd-archive-replay.test.ts +0 -82
  306. package/tests/sdd-classical-continuation.test.ts +0 -74
  307. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  308. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  309. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  310. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  311. package/tests/sdd-odd-integration.test.ts +0 -33
  312. package/tests/sdd-optional-research.test.ts +0 -124
  313. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  314. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  315. package/tests/sdd-preflight.test.ts +0 -541
  316. package/tests/sdd-research-capabilities.test.ts +0 -114
  317. package/tests/sdd-research-live.test.ts +0 -241
  318. package/tests/sdd-selection-transport.test.ts +0 -653
  319. package/tests/sdd-status.test.ts +0 -9
  320. package/tests/sdd-task-truth.test.ts +0 -43
@@ -0,0 +1,142 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import promptHistoryExtension from "../extensions/history/index.ts";
7
+ import { projectHash } from "../extensions/history/store.ts";
8
+
9
+ // Home/End routing in the history selector: with text in the search box
10
+ // they move the search caret (End never jumps or loads the list); with an
11
+ // empty search box they keep the documented list jumps (§B2/§D7). The real
12
+ // selector is driven through the history command with a fake overlay host;
13
+ // every fixture lives under os.tmpdir(), never the user's real ~/.pi.
14
+
15
+ const CWD = "/pi-history-fixtures/project-caret-keys";
16
+ const HOME = "\x1b[H";
17
+ const END = "\x1b[F";
18
+ const LEFT = "\x1b[D";
19
+ const ENTER = "\r";
20
+ // 15 prompts, oldest p00 .. newest p14: more than the initial 10-row window.
21
+ const PROMPT_COUNT = 15;
22
+
23
+ interface Selector {
24
+ handleInput(data: string): void;
25
+ }
26
+
27
+ function makeRoot(): string {
28
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-caret-"));
29
+ const dir = path.join(root, "projects", projectHash(CWD));
30
+ fs.mkdirSync(dir, { recursive: true });
31
+ const lines = Array.from({ length: PROMPT_COUNT }, (_, i) =>
32
+ JSON.stringify({
33
+ v: 1,
34
+ text: `p${String(i).padStart(2, "0")}`,
35
+ ts: 1_700_000_000_000 + i,
36
+ }),
37
+ );
38
+ fs.writeFileSync(path.join(dir, "store.jsonl"), `${lines.join("\n")}\n`);
39
+ return root;
40
+ }
41
+
42
+ /**
43
+ * Open the selector, feed `keys` one at a time, and return the prompt the
44
+ * selector pasted into the editor (null when it pasted nothing).
45
+ */
46
+ async function selectAfter(keys: string[]): Promise<string | null> {
47
+ const root = makeRoot();
48
+ const commands: Array<[string, { handler: unknown }]> = [];
49
+ promptHistoryExtension(
50
+ {
51
+ on: () => {},
52
+ registerShortcut: () => {},
53
+ registerCommand: (name: string, def: { handler: unknown }) => {
54
+ commands.push([name, def]);
55
+ },
56
+ } as never,
57
+ {
58
+ env: { GENTLE_PI_HISTORY_CAPTURE: "1" },
59
+ gentlePiConfigHome: fs.mkdtempSync(
60
+ path.join(os.tmpdir(), "pi-history-caret-config-"),
61
+ ),
62
+ root,
63
+ cwd: CWD,
64
+ instanceId: "inst-caret",
65
+ agentDir: path.join(root, "agent"),
66
+ sessionsRoot: path.join(root, "sessions"),
67
+ },
68
+ );
69
+ const command = commands.find(([name]) => name === "history");
70
+ assert.ok(command, "the history command must be registered");
71
+ const handler = command[1].handler as (
72
+ args: unknown,
73
+ ctx: unknown,
74
+ ) => Promise<void>;
75
+
76
+ let pasted: string | null = null;
77
+ const plain = (_color: string, text: string) => text;
78
+ const ctx = {
79
+ ui: {
80
+ notify: (message: string) => {
81
+ assert.fail(`unexpected notification: ${message}`);
82
+ },
83
+ pasteToEditor: (text: string) => {
84
+ pasted = text;
85
+ },
86
+ custom: (
87
+ factory: (
88
+ tui: unknown,
89
+ theme: unknown,
90
+ keybindings: unknown,
91
+ done: (result: unknown) => void,
92
+ ) => Selector,
93
+ ) =>
94
+ new Promise((resolve) => {
95
+ const selector = factory(
96
+ { requestRender: () => {} },
97
+ { fg: plain, bg: plain, bold: (text: string) => text },
98
+ undefined,
99
+ resolve,
100
+ );
101
+ for (const key of keys) selector.handleInput(key);
102
+ // Enter selects the highlighted row; with no match it resolves
103
+ // nothing, so close explicitly to finish the command.
104
+ selector.handleInput(ENTER);
105
+ resolve(null);
106
+ }),
107
+ },
108
+ };
109
+ await handler([], ctx);
110
+ // The paste path schedules one render tick; let it drain.
111
+ await new Promise((resolve) => setTimeout(resolve, 0));
112
+ return pasted;
113
+ }
114
+
115
+ test("with a query, Home moves the search caret instead of the list", async () => {
116
+ // Typed "1", caret to the start, then "p": the query is "p1", whose
117
+ // newest match is p14. A list jump would leave the query as "1p".
118
+ assert.equal(await selectAfter(["1", HOME, "p"]), "p14");
119
+ });
120
+
121
+ test("with a query, End moves the search caret instead of the list", async () => {
122
+ // Caret moved to the start, End brings it back: "p1" + "3" = "p13".
123
+ // A list jump would leave the caret at 0 and type "3p1".
124
+ assert.equal(await selectAfter(["p", "1", LEFT, LEFT, END, "3"]), "p13");
125
+ });
126
+
127
+ test("with a query, End neither jumps to the last match nor loads the list", async () => {
128
+ // A real query: the highlighted row stays the newest match, not p00.
129
+ assert.equal(await selectAfter(["p", END]), "p14");
130
+ // A whitespace-only query filters nothing and keeps the 10-row window:
131
+ // End must not load every record and select the oldest one.
132
+ assert.equal(await selectAfter([" ", END]), "p14");
133
+ });
134
+
135
+ test("with an empty query, Home and End keep the list jumps", async () => {
136
+ // End loads every record and selects the oldest one (§D7).
137
+ assert.equal(await selectAfter([END]), "p00");
138
+ // Home jumps back to the newest.
139
+ assert.equal(await selectAfter([END, HOME]), "p14");
140
+ // Emptying the query hands Home/End back to the list.
141
+ assert.equal(await selectAfter(["p", "\x7f", END]), "p00");
142
+ });
@@ -0,0 +1,170 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import {
7
+ bootstrapProjectSeed,
8
+ projectHash,
9
+ seedFilePath,
10
+ } from "../extensions/history/store.ts";
11
+
12
+ // Fake project cwd (never created on disk): projectHash falls back to
13
+ // raw-string hashing for nonexistent paths, and the transcript dirName
14
+ // encoding derives from the same string.
15
+ const CWD = "/pi-history-test/seed-project";
16
+ const DIR = "--pi-history-test-seed-project--";
17
+
18
+ function makeDirs(): { root: string; sessionsRoot: string } {
19
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), "pi-history-seed-"));
20
+ return {
21
+ root: path.join(base, "pi-history"),
22
+ sessionsRoot: path.join(base, "sessions"),
23
+ };
24
+ }
25
+
26
+ function writeSession(
27
+ sessionsRoot: string,
28
+ dirName: string,
29
+ fileName: string,
30
+ userTexts: string[],
31
+ mtimeMs?: number,
32
+ ): string {
33
+ const dir = path.join(sessionsRoot, dirName);
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ const file = path.join(dir, fileName);
36
+ const lines: string[] = [
37
+ JSON.stringify({
38
+ type: "session",
39
+ version: 1,
40
+ timestamp: "2026-01-01T00:00:00.000Z",
41
+ }),
42
+ ];
43
+ let ms = 1700000000000;
44
+ for (const text of userTexts) {
45
+ lines.push(
46
+ JSON.stringify({
47
+ type: "message",
48
+ timestamp: "2026-01-01T00:00:00.000Z",
49
+ message: { role: "user", content: text, timestamp: ms },
50
+ }),
51
+ );
52
+ ms += 1;
53
+ }
54
+ fs.writeFileSync(file, `${lines.join("\n")}\n`, "utf8");
55
+ if (mtimeMs !== undefined) {
56
+ fs.utimesSync(file, new Date(mtimeMs), new Date(mtimeMs));
57
+ }
58
+ return file;
59
+ }
60
+
61
+ function seedTexts(root: string): string[] {
62
+ const file = seedFilePath(root, CWD);
63
+ if (!fs.existsSync(file)) return [];
64
+ return fs
65
+ .readFileSync(file, "utf8")
66
+ .trim()
67
+ .split("\n")
68
+ .filter((l) => l.length > 0)
69
+ .map((l) => (JSON.parse(l) as { text: string }).text);
70
+ }
71
+
72
+ test("no sessions and no project dir: bootstrap seeds nothing", () => {
73
+ const { root, sessionsRoot } = makeDirs();
74
+ const result = bootstrapProjectSeed(root, CWD, sessionsRoot, 500);
75
+ assert.deepEqual(result, { seeded: 0, ran: false });
76
+ assert.equal(fs.existsSync(seedFilePath(root, CWD)), false);
77
+ });
78
+
79
+ test("empty project dir bootstraps from the project's transcripts", () => {
80
+ const { root, sessionsRoot } = makeDirs();
81
+ writeSession(sessionsRoot, DIR, "s1.jsonl", [
82
+ "real prompt",
83
+ "/compact",
84
+ " ",
85
+ "second prompt",
86
+ ]);
87
+ const result = bootstrapProjectSeed(root, CWD, sessionsRoot, 500);
88
+ assert.deepEqual(result, { seeded: 2, ran: true });
89
+ // Chronological order (oldest first) in the seed file.
90
+ assert.deepEqual(seedTexts(root), ["real prompt", "second prompt"]);
91
+ });
92
+
93
+ test("only the project's own session dir is scanned", () => {
94
+ const { root, sessionsRoot } = makeDirs();
95
+ writeSession(sessionsRoot, DIR, "s1.jsonl", ["mine"]);
96
+ writeSession(sessionsRoot, "--Other--", "s2.jsonl", ["not mine"]);
97
+ bootstrapProjectSeed(root, CWD, sessionsRoot, 500);
98
+ assert.deepEqual(seedTexts(root), ["mine"]);
99
+ });
100
+
101
+ test("caps at the target keeping the newest", () => {
102
+ const { root, sessionsRoot } = makeDirs();
103
+ const texts: string[] = [];
104
+ for (let i = 1; i <= 600; i++) texts.push(`p${i}`);
105
+ writeSession(sessionsRoot, DIR, "big.jsonl", texts);
106
+ const result = bootstrapProjectSeed(root, CWD, sessionsRoot, 500);
107
+ assert.deepEqual(result, { seeded: 500, ran: true });
108
+ const all = seedTexts(root);
109
+ assert.equal(all.length, 500);
110
+ assert.equal(all[0], "p101"); // oldest kept
111
+ assert.equal(all[499], "p600"); // newest
112
+ });
113
+
114
+ test("project dir already populated above target: no scan, seed untouched", () => {
115
+ const { root, sessionsRoot } = makeDirs();
116
+ const dir = path.join(root, "projects", projectHash(CWD));
117
+ fs.mkdirSync(dir, { recursive: true });
118
+ const existing = path.join(dir, "existing.jsonl");
119
+ fs.writeFileSync(
120
+ existing,
121
+ `${Array.from({ length: 500 }, (_, i) =>
122
+ JSON.stringify({ v: 1, text: `e${i}` }),
123
+ ).join("\n")}\n`,
124
+ "utf8",
125
+ );
126
+ const marker = writeSession(sessionsRoot, DIR, "s.jsonl", ["marker"]);
127
+ fs.utimesSync(
128
+ marker,
129
+ new Date(Date.now() + 5000),
130
+ new Date(Date.now() + 5000),
131
+ );
132
+ const result = bootstrapProjectSeed(root, CWD, sessionsRoot, 500);
133
+ assert.deepEqual(result, { seeded: 0, ran: false });
134
+ assert.deepEqual(seedTexts(root), []);
135
+ assert.equal(fs.readFileSync(existing, "utf8").includes("marker"), false);
136
+ });
137
+
138
+ // node:test has no test.skipIf (Bun-ism): root skips via the options
139
+ // object — chmod 000 is invisible to the superuser.
140
+ const sealedStoreTest = (name: string, fn: () => void) =>
141
+ test(
142
+ name,
143
+ { skip: process.getuid?.() === 0 ? "requires non-root" : false },
144
+ fn,
145
+ );
146
+ sealedStoreTest(
147
+ "an unreadable existing store file is skipped during counting; seeding still runs from transcripts",
148
+ () => {
149
+ const { root, sessionsRoot } = makeDirs();
150
+ const dir = path.join(root, "projects", projectHash(CWD));
151
+ fs.mkdirSync(dir, { recursive: true });
152
+ const sealed = path.join(dir, "sealed.jsonl");
153
+ fs.writeFileSync(
154
+ sealed,
155
+ `${JSON.stringify({ v: 1, text: "sealed-entry" })}\n`,
156
+ "utf8",
157
+ );
158
+ fs.chmodSync(sealed, 0o000);
159
+ writeSession(sessionsRoot, DIR, "s1.jsonl", ["from transcript"]);
160
+ try {
161
+ // The unreadable file contributes zero to existingCount, so the count
162
+ // stays under target and the transcript scan still runs. No throw.
163
+ const result = bootstrapProjectSeed(root, CWD, sessionsRoot, 500);
164
+ assert.deepEqual(result, { seeded: 1, ran: true });
165
+ assert.deepEqual(seedTexts(root), ["from transcript"]);
166
+ } finally {
167
+ fs.chmodSync(sealed, 0o644); // restore before cleanup
168
+ }
169
+ },
170
+ );
@@ -0,0 +1,129 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { bootstrapProjectSeed, seedFilePath } from "../extensions/history/store.ts";
7
+
8
+ // Fake project cwd (never created on disk): projectHash falls back to
9
+ // raw-string hashing for nonexistent paths, and the transcript dirName
10
+ // encoding derives from the same string.
11
+ const CWD = "/pi-history-test/seed-regen-project";
12
+ const DIR = "--pi-history-test-seed-regen-project--";
13
+
14
+ function setup() {
15
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), "seed2-"));
16
+ return {
17
+ root: path.join(base, "h"),
18
+ sessionsRoot: path.join(base, "sessions"),
19
+ stateDir: path.join(base, "state"),
20
+ };
21
+ }
22
+
23
+ function writeSession(sessionsRoot: string, texts: string[]): void {
24
+ const dir = path.join(sessionsRoot, DIR);
25
+ fs.mkdirSync(dir, { recursive: true });
26
+ const lines = [
27
+ JSON.stringify({
28
+ type: "session",
29
+ version: 1,
30
+ timestamp: "2026-01-01T00:00:00.000Z",
31
+ }),
32
+ ];
33
+ let ms = 1700000000000;
34
+ for (const text of texts) {
35
+ lines.push(
36
+ JSON.stringify({
37
+ type: "message",
38
+ timestamp: "2026-01-01T00:00:00.000Z",
39
+ message: { role: "user", content: text, timestamp: ms++ },
40
+ }),
41
+ );
42
+ }
43
+ fs.writeFileSync(path.join(dir, "s1.jsonl"), `${lines.join("\n")}\n`, "utf8");
44
+ }
45
+
46
+ test("an existing seed is never regenerated (deleted prompts stay gone)", () => {
47
+ const { root, sessionsRoot } = setup();
48
+ writeSession(sessionsRoot, ["keep", "delete-me"]);
49
+ bootstrapProjectSeed(root, CWD, sessionsRoot, 500);
50
+ // User deletes "delete-me" from the seed file (scope delete).
51
+ const seed = seedFilePath(root, CWD);
52
+ const kept = fs
53
+ .readFileSync(seed, "utf8")
54
+ .split("\n")
55
+ .filter((l) => !l.includes("delete-me"))
56
+ .join("\n");
57
+ fs.writeFileSync(seed, kept, "utf8");
58
+ // A NEW session bootstraps again -> must not resurrect from transcripts.
59
+ const again = bootstrapProjectSeed(root, CWD, sessionsRoot, 500);
60
+ assert.deepEqual(again, { seeded: 0, ran: false });
61
+ const texts = fs
62
+ .readFileSync(seed, "utf8")
63
+ .trim()
64
+ .split("\n")
65
+ .map((l) => (JSON.parse(l) as { text: string }).text);
66
+ assert.deepEqual(texts, ["keep"]);
67
+ });
68
+
69
+ test("untrusted tombstones defer seed until repaired, then suppress deleted prompts", () => {
70
+ const { root, sessionsRoot, stateDir } = setup();
71
+ writeSession(sessionsRoot, ["visible", "hidden-prompt"]);
72
+ fs.mkdirSync(stateDir, { recursive: true });
73
+ const hiddenFile = path.join(stateDir, "hidden.json");
74
+ fs.writeFileSync(hiddenFile, "{broken");
75
+ assert.deepEqual(bootstrapProjectSeed(root, CWD, sessionsRoot, 500, stateDir), {
76
+ seeded: 0, ran: false,
77
+ });
78
+ assert.equal(fs.existsSync(seedFilePath(root, CWD)), false);
79
+ fs.writeFileSync(hiddenFile, JSON.stringify(["hidden-prompt"]));
80
+ assert.deepEqual(bootstrapProjectSeed(root, CWD, sessionsRoot, 500, stateDir), {
81
+ seeded: 1, ran: true,
82
+ });
83
+ assert.deepEqual(
84
+ fs.readFileSync(seedFilePath(root, CWD), "utf8").trim().split("\n")
85
+ .map((line) => (JSON.parse(line) as { text: string }).text),
86
+ ["visible"],
87
+ );
88
+ });
89
+
90
+ test("a failed seed rename leaves no gate and retries with all prompts", () => {
91
+ const { root, sessionsRoot, stateDir } = setup();
92
+ writeSession(sessionsRoot, ["visible", "hidden-prompt"]);
93
+ fs.mkdirSync(stateDir, { recursive: true });
94
+ fs.writeFileSync(path.join(stateDir, "hidden.json"), JSON.stringify(["hidden-prompt"]));
95
+ const seed = seedFilePath(root, CWD);
96
+ const original = fs.renameSync;
97
+ fs.renameSync = ((from: fs.PathLike, to: fs.PathLike) => {
98
+ if (to === seed) throw new Error("injected seed rename failure");
99
+ return original(from, to);
100
+ }) as typeof fs.renameSync;
101
+ try {
102
+ assert.throws(() => bootstrapProjectSeed(root, CWD, sessionsRoot, 500, stateDir), /injected/);
103
+ assert.equal(fs.existsSync(seed), false);
104
+ } finally {
105
+ fs.renameSync = original;
106
+ }
107
+ assert.deepEqual(bootstrapProjectSeed(root, CWD, sessionsRoot, 500, stateDir), {
108
+ seeded: 1, ran: true,
109
+ });
110
+ });
111
+
112
+ test("tombstoned prompts are not seeded from transcripts", () => {
113
+ const { root, sessionsRoot, stateDir } = setup();
114
+ writeSession(sessionsRoot, ["visible", "hidden-prompt"]);
115
+ // Tombstone "hidden-prompt" (same key shape hide-prompts writes).
116
+ fs.mkdirSync(stateDir, { recursive: true });
117
+ fs.writeFileSync(
118
+ path.join(stateDir, "hidden.json"),
119
+ JSON.stringify(["hidden-prompt"]),
120
+ "utf8",
121
+ );
122
+ bootstrapProjectSeed(root, CWD, sessionsRoot, 500, stateDir);
123
+ const texts = fs
124
+ .readFileSync(seedFilePath(root, CWD), "utf8")
125
+ .trim()
126
+ .split("\n")
127
+ .map((l) => (JSON.parse(l) as { text: string }).text);
128
+ assert.deepEqual(texts, ["visible"]);
129
+ });
@@ -0,0 +1,94 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ buildPromptRecords,
5
+ clampPreviewOffset,
6
+ clampSelectedIndex,
7
+ computeVisibleRange,
8
+ getVisiblePromptRecords,
9
+ moveSelectedIndex,
10
+ pageSelectedIndex,
11
+ } from "../extensions/history/selector-helpers.ts";
12
+
13
+ test("buildPromptRecords lowercases search text", () => {
14
+ assert.deepEqual(buildPromptRecords(["Hello"]), [
15
+ { text: "Hello", searchText: "hello" },
16
+ ]);
17
+ });
18
+
19
+ test("computeVisibleRange centers when possible", () => {
20
+ assert.deepEqual(computeVisibleRange(8, 30, 10), { start: 3, end: 13 });
21
+ });
22
+
23
+ test("computeVisibleRange pins near end", () => {
24
+ assert.deepEqual(computeVisibleRange(28, 30, 10), { start: 20, end: 30 });
25
+ });
26
+
27
+ test("computeVisibleRange handles small lists", () => {
28
+ assert.deepEqual(computeVisibleRange(1, 3, 10), { start: 0, end: 3 });
29
+ });
30
+
31
+ test("clampSelectedIndex stays within filtered record bounds", () => {
32
+ assert.equal(clampSelectedIndex(8, 3), 2);
33
+ assert.equal(clampSelectedIndex(1, 0), 0);
34
+ });
35
+
36
+ test("clampPreviewOffset clamps offsets past the last page", () => {
37
+ assert.equal(clampPreviewOffset(50, 47, 10), 37);
38
+ assert.equal(clampPreviewOffset(5, 47, 10), 5);
39
+ });
40
+
41
+ test("clampPreviewOffset pins to zero when content fits the viewport", () => {
42
+ assert.equal(clampPreviewOffset(3, 8, 10), 0);
43
+ assert.equal(clampPreviewOffset(7, 0, 10), 0);
44
+ });
45
+
46
+ test("moveSelectedIndex wraps around the list", () => {
47
+ assert.equal(moveSelectedIndex(0, 3, -1), 2);
48
+ assert.equal(moveSelectedIndex(2, 3, 1), 0);
49
+ assert.equal(moveSelectedIndex(0, 0, 1), 0);
50
+ });
51
+
52
+ test("pageSelectedIndex clamps within the list", () => {
53
+ assert.equal(pageSelectedIndex(8, 30, -10), 0);
54
+ assert.equal(pageSelectedIndex(2, 3, 10), 2);
55
+ assert.equal(pageSelectedIndex(0, 0, 10), 0);
56
+ });
57
+
58
+ test("pageSelectedIndex end-clamps a downward page at the last entry (AC-P2-1.1)", () => {
59
+ assert.equal(pageSelectedIndex(28, 30, 10), 29);
60
+ assert.equal(pageSelectedIndex(25, 30, 10), 29);
61
+ });
62
+
63
+ test("pageSelectedIndex clamps |pageSize| greater than total in both directions (AC-P2-1.2)", () => {
64
+ assert.equal(pageSelectedIndex(0, 3, 10), 2);
65
+ assert.equal(pageSelectedIndex(2, 3, -10), 0);
66
+ assert.equal(pageSelectedIndex(0, 30, -50), 0);
67
+ });
68
+
69
+ test("pageSelectedIndex never wraps past the ends (AC-P2-1.2)", () => {
70
+ assert.equal(pageSelectedIndex(29, 30, 10), 29);
71
+ assert.equal(pageSelectedIndex(0, 30, -10), 0);
72
+ });
73
+
74
+ test("getVisiblePromptRecords returns visible records with selection state", () => {
75
+ assert.deepEqual(
76
+ getVisiblePromptRecords(
77
+ buildPromptRecords(["one", "two", "three", "four"]),
78
+ 2,
79
+ 2,
80
+ ),
81
+ [
82
+ {
83
+ index: 1,
84
+ record: { text: "two", searchText: "two" },
85
+ isSelected: false,
86
+ },
87
+ {
88
+ index: 2,
89
+ record: { text: "three", searchText: "three" },
90
+ isSelected: true,
91
+ },
92
+ ],
93
+ );
94
+ });
@@ -0,0 +1,87 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { listSessionFiles } from "../extensions/history/session-scan.ts";
7
+
8
+ /**
9
+ * WU1b-carried T7 (AC-S1-7): the one-level directory exclusion matrix. The
10
+ * fixture tree mirrors the pi sessions root — encoded-cwd directories with
11
+ * top-level jsonl session files, nested run-N/session.jsonl subagent
12
+ * payloads, a subagent-artifacts subtree, and a stray root-level file.
13
+ * Fixture files carry garbage content: the scanner must LIST paths only, so
14
+ * exact path equality proves nested payloads are never ingested (a
15
+ * recursive scanner would emit them).
16
+ */
17
+ function makeSessionsRoot(): string {
18
+ return fs.mkdtempSync(path.join(os.tmpdir(), "session-scan-dirs-"));
19
+ }
20
+
21
+ function writeFileAt(filePath: string): void {
22
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
23
+ fs.writeFileSync(filePath, "garbage-not-json\n", "utf8");
24
+ }
25
+
26
+ test("one-level scan rule: only top-level jsonl of cwd dirs; nested payloads, subagent-artifacts, and stray root files never ingested (AC-S1-7)", () => {
27
+ const root = makeSessionsRoot();
28
+
29
+ // two cwd directories, each holding top-level jsonl session files
30
+ const cwdA = path.join(root, "--home-user-project-a--");
31
+ const cwdB = path.join(root, "--home-user-project-b--");
32
+ writeFileAt(path.join(cwdA, "2026-01-01t10-00-00aaa.jsonl"));
33
+ writeFileAt(path.join(cwdA, "2026-01-02t11-00-00bbb.jsonl"));
34
+ writeFileAt(path.join(cwdB, "2026-01-03t12-00-00ccc.jsonl"));
35
+
36
+ // nested run-N/session.jsonl subagent payloads — never descended
37
+ writeFileAt(path.join(cwdA, "run-1", "session.jsonl"));
38
+ writeFileAt(path.join(cwdB, "run-2", "session.jsonl"));
39
+
40
+ // a subagent-artifacts subtree holding a jsonl file — never descended
41
+ writeFileAt(path.join(cwdA, "subagent-artifacts", "artifact.jsonl"));
42
+
43
+ // one stray root-level FILE (not a directory) — skipped
44
+ writeFileAt(path.join(root, "stray.jsonl"));
45
+
46
+ const files = listSessionFiles(root);
47
+
48
+ // exactly the three top-level jsonl paths of the cwd directories,
49
+ // sorted, absolute
50
+ assert.deepEqual(files, [
51
+ path.join(cwdA, "2026-01-01t10-00-00aaa.jsonl"),
52
+ path.join(cwdA, "2026-01-02t11-00-00bbb.jsonl"),
53
+ path.join(cwdB, "2026-01-03t12-00-00ccc.jsonl"),
54
+ ]);
55
+ for (const file of files) {
56
+ assert.equal(path.isAbsolute(file), true);
57
+ }
58
+ });
59
+
60
+ // node:test has no test.skipIf (Bun-ism): root skips via the options
61
+ // object — chmod 000 is invisible to the superuser.
62
+ const sealedDirTest = (name: string, fn: () => void) =>
63
+ test(
64
+ name,
65
+ { skip: process.getuid?.() === 0 ? "requires non-root" : false },
66
+ fn,
67
+ );
68
+ sealedDirTest(
69
+ "an unreadable child dir (chmod 000) is skipped; sibling dirs still list",
70
+ () => {
71
+ const root = makeSessionsRoot();
72
+ const sealed = path.join(root, "--sealed--");
73
+ const open = path.join(root, "--open--");
74
+ writeFileAt(path.join(sealed, "hidden-session.jsonl"));
75
+ writeFileAt(path.join(open, "visible-session.jsonl"));
76
+ fs.chmodSync(sealed, 0o000);
77
+ try {
78
+ // One directory whose readdir fails skips itself — never fatal — and
79
+ // the sibling directories still contribute their files.
80
+ assert.deepEqual(listSessionFiles(root), [
81
+ path.join(open, "visible-session.jsonl"),
82
+ ]);
83
+ } finally {
84
+ fs.chmodSync(sealed, 0o755); // restore before cleanup
85
+ }
86
+ },
87
+ );