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,110 @@
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
+ drainGlobal,
8
+ drainProject,
9
+ globalSeedPath,
10
+ projectHash,
11
+ type DrainResult,
12
+ } from "../extensions/history/store.ts";
13
+
14
+ // Portable project identity: a never-existing literal. projectHash falls
15
+ // back to hashing the raw string when realpath fails, so the identity is
16
+ // deterministic on every machine (no machine-specific absolute paths).
17
+
18
+ const CWD = "/pi-history-test/drain-hidden-project";
19
+
20
+ function write(file: string, texts: string[], ts = 100): void {
21
+ fs.mkdirSync(path.dirname(file), { recursive: true });
22
+ fs.writeFileSync(
23
+ file,
24
+ `${texts.map((t) => JSON.stringify({ v: 1, text: t, ts })).join("\n")}\n`,
25
+ "utf8",
26
+ );
27
+ }
28
+
29
+ // Unwrap the ok shape. Drains FAIL CLOSED: the blocked variant carries no
30
+ // prompts field at all (asserted in the blocked test below).
31
+ function okPrompts(result: DrainResult): string[] {
32
+ assert.equal(result.status, "ok");
33
+ if (result.status !== "ok") throw new Error("unreachable");
34
+ return result.prompts;
35
+ }
36
+
37
+ test("drains skip tombstoned prompts in seeds and session files", () => {
38
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), "hid-"));
39
+ const root = path.join(base, "h");
40
+ const stateDir = path.join(base, "state");
41
+ fs.mkdirSync(stateDir, { recursive: true });
42
+ fs.writeFileSync(
43
+ path.join(stateDir, "hidden.json"),
44
+ JSON.stringify(["deleted from seed", "deleted from session"]),
45
+ "utf8",
46
+ );
47
+ const dir = path.join(root, "projects", projectHash(CWD));
48
+ write(path.join(dir, "seed.jsonl"), ["keep", "deleted from seed"], 100);
49
+ write(path.join(dir, "s1.jsonl"), ["also keep", "deleted from session"], 200);
50
+ write(globalSeedPath(root), ["deleted from seed", "legacy keep"], 50);
51
+
52
+ assert.deepEqual(okPrompts(drainProject(root, CWD, 1000, stateDir)), [
53
+ "also keep",
54
+ "keep",
55
+ ]);
56
+ assert.deepEqual(okPrompts(drainGlobal(root, 1000, stateDir)), [
57
+ "also keep",
58
+ "keep",
59
+ "legacy keep",
60
+ ]);
61
+ // Without a stateDir the filter is off (raw drain semantics).
62
+ assert.equal(
63
+ okPrompts(drainProject(root, CWD)).includes("deleted from seed"),
64
+ true,
65
+ );
66
+ });
67
+
68
+ // Fail-closed seam: an untrusted hidden.json BLOCKS both drains with the
69
+ // recovery message and no prompts field; a stateDir whose hidden.json is
70
+ // MISSING stays the safe empty-tombstones case (the full expected prompts).
71
+ test("corrupt hidden.json blocks both drains with no prompts field; a missing file drains normally", () => {
72
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), "hid-blocked-"));
73
+ const root = path.join(base, "h");
74
+ const stateDir = path.join(base, "state");
75
+ fs.mkdirSync(stateDir, { recursive: true });
76
+ fs.writeFileSync(
77
+ path.join(stateDir, "hidden.json"),
78
+ "{corrupt bytes",
79
+ "utf8",
80
+ );
81
+ const dir = path.join(root, "projects", projectHash(CWD));
82
+ write(path.join(dir, "seed.jsonl"), ["secret prompt", "keeper"], 100);
83
+
84
+ for (const result of [
85
+ drainProject(root, CWD, 1000, stateDir),
86
+ drainGlobal(root, 1000, stateDir),
87
+ ]) {
88
+ assert.equal(result.status, "blocked");
89
+ if (result.status !== "blocked") throw new Error("unreachable");
90
+ assert.ok(result.message.includes("hidden.json"));
91
+ // The blocked shape carries no prompts to render.
92
+ assert.equal("prompts" in result, false);
93
+ }
94
+
95
+ // Missing file: safe empty tombstones — the full drain comes back.
96
+ const missingBase = fs.mkdtempSync(path.join(os.tmpdir(), "hid-missing-"));
97
+ const missingRoot = path.join(missingBase, "h");
98
+ const missingState = path.join(missingBase, "state");
99
+ fs.mkdirSync(missingState, { recursive: true });
100
+ const missingDir = path.join(missingRoot, "projects", projectHash(CWD));
101
+ write(path.join(missingDir, "seed.jsonl"), ["kept", "shown"], 100);
102
+ assert.deepEqual(
103
+ okPrompts(drainProject(missingRoot, CWD, 1000, missingState)),
104
+ ["shown", "kept"],
105
+ );
106
+ assert.deepEqual(okPrompts(drainGlobal(missingRoot, 1000, missingState)), [
107
+ "shown",
108
+ "kept",
109
+ ]);
110
+ });
@@ -0,0 +1,98 @@
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
+ drainGlobal,
8
+ drainProject,
9
+ globalSeedPath,
10
+ projectHash,
11
+ type DrainResult,
12
+ } from "../extensions/history/store.ts";
13
+
14
+ // Portable project identity: a never-existing literal. projectHash falls
15
+ // back to hashing the raw string when realpath fails, so the identity is
16
+ // deterministic on every machine (no machine-specific absolute paths).
17
+
18
+ const CWD = "/pi-history-test/drain-order-project";
19
+
20
+ function writeTs(file: string, texts: string[], ts: number): void {
21
+ fs.mkdirSync(path.dirname(file), { recursive: true });
22
+ fs.writeFileSync(
23
+ file,
24
+ `${texts.map((t) => JSON.stringify({ v: 1, text: t, ts })).join("\n")}\n`,
25
+ "utf8",
26
+ );
27
+ }
28
+
29
+ // Mechanical unwrap of the ok shape (drains can also return blocked).
30
+ function okPrompts(result: DrainResult): string[] {
31
+ assert.equal(result.status, "ok");
32
+ if (result.status !== "ok") throw new Error("unreachable");
33
+ return result.prompts;
34
+ }
35
+
36
+ test("atomic rewrite (delete) does not reshuffle the drain order", () => {
37
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "ord-"));
38
+ const dir = path.join(root, "projects", projectHash(CWD));
39
+ writeTs(path.join(dir, "old.jsonl"), ["a-old"], 100);
40
+ writeTs(path.join(dir, "new.jsonl"), ["z-new"], 200);
41
+ assert.deepEqual(okPrompts(drainProject(root, CWD)), ["z-new", "a-old"]);
42
+ // Slice 5 ports deleteFromProject; its observable effect on the drain is
43
+ // simulated directly here: an atomic rewrite of the affected file that
44
+ // empties it — the mtime jumps to NOW, and the drain order must not move.
45
+ fs.writeFileSync(path.join(dir, "old.jsonl"), "", "utf8");
46
+ fs.utimesSync(path.join(dir, "old.jsonl"), new Date(), new Date());
47
+ assert.deepEqual(okPrompts(drainProject(root, CWD)), ["z-new"]);
48
+ // Re-add with an OLD ts via direct write: still ordered by ts, not mtime.
49
+ writeTs(path.join(dir, "old2.jsonl"), ["b-old"], 150);
50
+ fs.utimesSync(
51
+ path.join(dir, "old2.jsonl"),
52
+ new Date(Date.now() + 99999),
53
+ new Date(Date.now() + 99999),
54
+ );
55
+ assert.deepEqual(okPrompts(drainProject(root, CWD)), ["z-new", "b-old"]);
56
+ });
57
+
58
+ test("global drain puts the legacy seed last regardless of its fresh mtime", () => {
59
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "seed-"));
60
+ const dir = path.join(root, "projects", projectHash(CWD));
61
+ writeTs(path.join(dir, "s.jsonl"), ["fresh"], 200);
62
+ const seed = globalSeedPath(root);
63
+ writeTs(seed, ["legacy-1", "legacy-2"], 10);
64
+ fs.utimesSync(seed, new Date(Date.now() + 5000), new Date(Date.now() + 5000));
65
+ assert.deepEqual(okPrompts(drainGlobal(root)), [
66
+ "fresh",
67
+ "legacy-2",
68
+ "legacy-1",
69
+ ]);
70
+ });
71
+
72
+ test(
73
+ "an unreadable store file is skipped; the rest drain in the expected order",
74
+ { skip: process.getuid?.() === 0 },
75
+ () => {
76
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), "ord-sealed-"));
77
+ const dir = path.join(root, "projects", projectHash(CWD));
78
+ writeTs(path.join(dir, "old.jsonl"), ["a-old"], 100);
79
+ const sealed = path.join(dir, "sealed.jsonl");
80
+ writeTs(sealed, ["sealed-never"], 150);
81
+ writeTs(path.join(dir, "new.jsonl"), ["z-new"], 200);
82
+ // The sealed file's fresh mtime would sort it FIRST if it were readable —
83
+ // its absence from the drain is caused by the unreadable skip alone.
84
+ fs.utimesSync(
85
+ sealed,
86
+ new Date(Date.now() + 99999),
87
+ new Date(Date.now() + 99999),
88
+ );
89
+ fs.chmodSync(sealed, 0o000);
90
+ try {
91
+ // An unreadable file reads as zero entries and drops out of the drain;
92
+ // the readable files keep their ts order. No throw.
93
+ assert.deepEqual(okPrompts(drainProject(root, CWD)), ["z-new", "a-old"]);
94
+ } finally {
95
+ fs.chmodSync(sealed, 0o644); // restore before cleanup
96
+ }
97
+ },
98
+ );
@@ -0,0 +1,62 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ type PiHistoryGlobals,
5
+ withExpandedHistoryGlobals,
6
+ } from "../extensions/history/selector-helpers.ts";
7
+
8
+ // withExpandedHistoryGlobals contract: optional hooks invoked via `?.` —
9
+ // expand exactly once BEFORE the run starts, trim exactly once in the
10
+ // finally (success and rejection alike), and the run's resolution passes
11
+ // through untouched. Fixtures track call order in one array so the
12
+ // before/after ordering and the call counts are pinned together.
13
+
14
+ function trackingGlobals(): { globals: PiHistoryGlobals; events: string[] } {
15
+ const events: string[] = [];
16
+ return {
17
+ events,
18
+ globals: {
19
+ __piHistoryExpand: () => {
20
+ events.push("expand");
21
+ },
22
+ __piHistoryTrim: () => {
23
+ events.push("trim");
24
+ },
25
+ },
26
+ };
27
+ }
28
+
29
+ test("expand runs once before the run; trim once after; the result passes through", async () => {
30
+ const { globals, events } = trackingGlobals();
31
+ let ran = 0;
32
+ const value = await withExpandedHistoryGlobals(globals, async () => {
33
+ // By the time the body executes, expand already ran — exactly once.
34
+ ran += 1;
35
+ assert.deepEqual(events, ["expand"]);
36
+ return 42;
37
+ });
38
+ assert.equal(value, 42);
39
+ assert.equal(ran, 1);
40
+ assert.deepEqual(events, ["expand", "trim"]);
41
+ });
42
+
43
+ test("a rejected run still trims (finally) and the rejection propagates unchanged", async () => {
44
+ const { globals, events } = trackingGlobals();
45
+ const boom = new Error("boom");
46
+ let caught: unknown;
47
+ try {
48
+ await withExpandedHistoryGlobals(globals, async () => {
49
+ throw boom;
50
+ });
51
+ } catch (error) {
52
+ caught = error;
53
+ }
54
+ assert.equal(caught, boom);
55
+ assert.deepEqual(events, ["expand", "trim"]);
56
+ });
57
+
58
+ test("absent hooks are tolerated: the run executes with no throw", async () => {
59
+ const empty: PiHistoryGlobals = {};
60
+ const value = await withExpandedHistoryGlobals(empty, async () => "ok");
61
+ assert.equal(value, "ok");
62
+ });