gentle-pi 3.7.0 → 4.1.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 (590) hide show
  1. package/README.md +155 -10
  2. package/assets/agents/gentle-ai-explore.md +13 -6
  3. package/assets/agents/gentle-ai-verify.md +27 -6
  4. package/assets/agents/gentle-ai-worker.md +13 -15
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +75 -169
  9. package/assets/orchestrator-memory.md +12 -26
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-skills.md +1 -1
  12. package/assets/orchestrator-tracking.md +53 -0
  13. package/assets/orchestrator-verification.md +28 -0
  14. package/assets/orchestrator-writer.md +38 -0
  15. package/assets/orchestrator.md +30 -38
  16. package/assets/sounds/LICENSE.md +52 -0
  17. package/assets/sounds/attention.wav +0 -0
  18. package/assets/sounds/error.wav +0 -0
  19. package/assets/sounds/success.wav +0 -0
  20. package/assets/support/strict-tdd-verify.md +4 -266
  21. package/assets/support/strict-tdd.md +8 -360
  22. package/bin/engram-guard +9 -0
  23. package/bin/gentle-shell-install.mjs +302 -0
  24. package/bin/gentle-shell.mjs +956 -50
  25. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  27. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  28. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  29. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  30. package/docs/bundled-install.md +397 -0
  31. package/docs/delegated-verification.md +27 -4
  32. package/docs/gentle-agents-activity.md +537 -0
  33. package/docs/gentle-shell-uninstall.md +136 -0
  34. package/docs/gentle-shell.md +165 -51
  35. package/docs/install-wizard.md +2040 -0
  36. package/docs/native-authority-architecture.md +3 -3
  37. package/docs/permission-modes.md +82 -0
  38. package/docs/prompt-history.md +280 -0
  39. package/docs/readme-reference.md +364 -231
  40. package/docs/render-cache-performance.md +28 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/telemetry.md +1 -1
  44. package/docs/yolo-mode.md +86 -0
  45. package/extensions/child-context.ts +35 -0
  46. package/extensions/child-safety.ts +25 -0
  47. package/extensions/codegraph-tools.ts +18 -5
  48. package/extensions/gentle-agents.ts +1093 -326
  49. package/extensions/gentle-ai.ts +1636 -890
  50. package/extensions/gentle-herdr-activity.ts +81 -0
  51. package/extensions/gentle-notifications.ts +104 -0
  52. package/extensions/gentle-shell.ts +1619 -136
  53. package/extensions/gentle-stats.ts +101 -0
  54. package/extensions/gentle-todo.ts +126 -20
  55. package/extensions/history/atomic-write.ts +38 -0
  56. package/extensions/history/hide-prompts.ts +183 -0
  57. package/extensions/history/index.ts +1424 -0
  58. package/extensions/history/load-shared-history.ts +39 -0
  59. package/extensions/history/selector-helpers.ts +538 -0
  60. package/extensions/history/session-scan.ts +233 -0
  61. package/extensions/history/store.ts +1119 -0
  62. package/extensions/nan-provider.ts +6 -0
  63. package/extensions/pi-pretty.ts +39 -3
  64. package/extensions/quiet-tools.ts +254 -92
  65. package/extensions/resume-hint.ts +60 -0
  66. package/extensions/skill-registry.ts +191 -58
  67. package/extensions/startup-banner.ts +71 -33
  68. package/lib/agent-assets.ts +604 -0
  69. package/lib/agent-home.ts +10 -1
  70. package/lib/agent-model-resolution.ts +80 -0
  71. package/lib/agent-profile-pin.ts +12 -0
  72. package/lib/agent-profiles.ts +13 -3
  73. package/lib/agents-completion-delivery.ts +11 -3
  74. package/lib/agents-message-delivery.ts +181 -0
  75. package/lib/agents-messaging.ts +1 -1
  76. package/lib/agents-protocol.ts +99 -0
  77. package/lib/agents-runner.ts +180 -107
  78. package/lib/agents-view.ts +26 -3
  79. package/lib/agents-widget.ts +16 -9
  80. package/lib/append-system-prompt.ts +21 -0
  81. package/lib/auto-permission-classifier.ts +280 -0
  82. package/lib/auto-permission-runtime.ts +268 -0
  83. package/lib/background-jobs-tools.ts +202 -0
  84. package/lib/background-jobs.ts +253 -0
  85. package/lib/background-monitor.ts +154 -0
  86. package/lib/bounded-writer-admission.ts +220 -0
  87. package/lib/builtin-codemode-optout.ts +166 -0
  88. package/lib/card-content-policy.ts +85 -0
  89. package/lib/card-style-policy.ts +60 -0
  90. package/lib/child-context-files.ts +166 -0
  91. package/lib/child-package-injection.ts +62 -0
  92. package/lib/codemode-renderer.ts +195 -0
  93. package/lib/command-palette-catalog.ts +5 -10
  94. package/lib/command-palette.ts +25 -14
  95. package/lib/context-threshold.ts +26 -0
  96. package/lib/destructive-command-guard.ts +144 -0
  97. package/lib/gentle-ai-binary.ts +109 -4
  98. package/lib/gentle-ai-elapsed-store.ts +87 -0
  99. package/lib/gentle-ai-renderer.ts +196 -39
  100. package/lib/gentle-shell-bundled.ts +517 -0
  101. package/lib/gentle-shell-launcher.ts +89 -27
  102. package/lib/gentle-shell-resume-hint.ts +176 -0
  103. package/lib/gentle-shell-startup-flow.ts +54 -0
  104. package/lib/gentle-shell-startup-update.ts +92 -0
  105. package/lib/gentle-shell-uninstall.ts +840 -0
  106. package/lib/gentle-shell-update-panel.ts +231 -0
  107. package/lib/herdr-activity.ts +183 -0
  108. package/lib/history-capture-policy.ts +95 -0
  109. package/lib/inprocess-reviewer.ts +3 -0
  110. package/lib/jobs-sidebar-state.ts +20 -0
  111. package/lib/jobs-view.ts +161 -0
  112. package/lib/model-price-ratio.ts +83 -0
  113. package/lib/model-routing-authority.ts +5 -1
  114. package/lib/nan-provider.ts +249 -0
  115. package/lib/native-review-cli.ts +242 -180
  116. package/lib/notification-audio-native.ts +162 -0
  117. package/lib/notification-audio-windows.ts +399 -0
  118. package/lib/notification-audio.ts +332 -0
  119. package/lib/notification-customize.ts +241 -0
  120. package/lib/notification-events.ts +113 -0
  121. package/lib/notification-policy.ts +148 -0
  122. package/lib/notification-pulse-client.ts +367 -0
  123. package/lib/notification-pulse-protocol.ts +305 -0
  124. package/lib/notification-pulse-stream.ts +213 -0
  125. package/lib/notification-pulse-worker.ts +66 -0
  126. package/lib/notification-scheduler.ts +173 -0
  127. package/lib/notification-service.ts +202 -0
  128. package/lib/notification-ui.ts +87 -0
  129. package/lib/odd-phase-inference.ts +231 -0
  130. package/lib/odd-phase.ts +141 -0
  131. package/lib/orchestrator-alias.ts +20 -0
  132. package/lib/orchestrator-catalog.ts +79 -0
  133. package/lib/orchestrator-consultation.ts +61 -0
  134. package/lib/orchestrator-discovery.ts +49 -0
  135. package/lib/orchestrator-helper-consent.ts +122 -0
  136. package/lib/orchestrator-helper.ts +167 -0
  137. package/lib/orchestrator-message-card.ts +105 -0
  138. package/lib/orchestrator-presence.ts +115 -14
  139. package/lib/orchestrator-scope.ts +80 -0
  140. package/lib/orchestrator-state.ts +107 -0
  141. package/lib/orchestrator-tool-card.ts +280 -0
  142. package/lib/orchestrator-work-search.ts +218 -0
  143. package/lib/orchestrator-work.ts +79 -0
  144. package/lib/overlay-repaint.ts +26 -0
  145. package/lib/permission-mode-policy.ts +59 -0
  146. package/lib/pi-tui-keys.ts +53 -0
  147. package/lib/review-candidate-view-owner.ts +73 -21
  148. package/lib/review-candidate-view.ts +115 -27
  149. package/lib/review-compact-contract.ts +2 -2
  150. package/lib/review-host-relay.ts +7 -2
  151. package/lib/review-integration-v2.ts +100 -22
  152. package/lib/review-publication-gate.ts +1 -1
  153. package/lib/review-reminder-receipt.ts +48 -8
  154. package/lib/review-risk-assessment.ts +227 -56
  155. package/lib/review-sidebar-state.ts +224 -0
  156. package/lib/review-transaction.ts +72 -3
  157. package/lib/runtime-metrics-children.ts +1 -1
  158. package/lib/runtime-metrics-native.ts +3 -2
  159. package/lib/selection-engine.ts +527 -0
  160. package/lib/session-change-capture.ts +5 -4
  161. package/lib/session-changes.ts +27 -22
  162. package/lib/session-messaging-grants.ts +135 -0
  163. package/lib/session-profile-binding.ts +83 -0
  164. package/lib/session-profile-freeze.ts +266 -0
  165. package/lib/session-worktree-registry.ts +87 -9
  166. package/lib/shell-bar.ts +215 -30
  167. package/lib/shell-card.ts +287 -18
  168. package/lib/shell-changes-view.ts +42 -14
  169. package/lib/shell-changes.ts +86 -71
  170. package/lib/shell-prompt.ts +102 -6
  171. package/lib/shell-sidebar-layout.ts +83 -15
  172. package/lib/shell-sidebar.ts +15 -1
  173. package/lib/shell-todo.ts +84 -31
  174. package/lib/shell-usage-view.ts +9 -4
  175. package/lib/shell-usage.ts +70 -13
  176. package/lib/stats-collector.ts +381 -0
  177. package/lib/stats-view.ts +431 -0
  178. package/lib/status-timing-diagnostics.ts +201 -0
  179. package/lib/telemetry-trigger.ts +3 -1
  180. package/lib/theme-customization.ts +52 -0
  181. package/lib/vim-editor-adapter.ts +383 -0
  182. package/lib/vim-normal-engine.ts +154 -0
  183. package/lib/vim-operator-engine.ts +416 -0
  184. package/lib/vim-policy.ts +49 -0
  185. package/lib/vim-visual-engine.ts +107 -0
  186. package/lib/visual-customization-policy.ts +119 -0
  187. package/lib/visual-customize-view.ts +481 -0
  188. package/lib/visual-profiles.ts +228 -0
  189. package/lib/writer-surfaces.ts +162 -0
  190. package/lib/yolo-session-policy.ts +317 -0
  191. package/package.json +20 -8
  192. package/runtime/child-package-injection.mjs +63 -0
  193. package/runtime/gentle-ai-binary.mjs +109 -4
  194. package/runtime/gentle-shell-bundled.mjs +518 -0
  195. package/runtime/gentle-shell-launcher.mjs +87 -25
  196. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  197. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  198. package/runtime/gentle-shell-startup-update.mjs +93 -0
  199. package/runtime/gentle-shell-uninstall.mjs +841 -0
  200. package/runtime/gentle-shell-update-panel.mjs +232 -0
  201. package/runtime/native-review-cli.mjs +239 -177
  202. package/runtime/review-integration-v2.mjs +100 -22
  203. package/runtime/review-risk-assessment.mjs +225 -54
  204. package/runtime/status-timing-diagnostics.mjs +202 -0
  205. package/runtime/telemetry-trigger.mjs +3 -1
  206. package/scripts/bootstrap.cmd +255 -0
  207. package/scripts/bootstrap.sh +168 -0
  208. package/scripts/build-distribution.mjs +157 -0
  209. package/scripts/build-installer-bundles.mjs +136 -0
  210. package/scripts/build-runtime-modules.mjs +8 -0
  211. package/scripts/bundled-install.mjs +1308 -0
  212. package/scripts/bundled-wizard.mjs +399 -0
  213. package/scripts/check-types.mjs +4 -6
  214. package/scripts/gentle-ai-installer.mjs +83 -35
  215. package/scripts/install-gentle-ai.mjs +48 -1
  216. package/scripts/install-tui-mode-setting.mjs +10 -1
  217. package/scripts/install-wizard-preview.mjs +154 -0
  218. package/scripts/installer-downloads.mjs +648 -0
  219. package/scripts/installer-preflight.mjs +387 -0
  220. package/scripts/installer-probes.mjs +564 -0
  221. package/scripts/installer-runner.mjs +1102 -0
  222. package/scripts/installer-server.mjs +881 -0
  223. package/scripts/installer-windows-artifacts.json +15 -0
  224. package/scripts/installer-windows.mjs +752 -0
  225. package/scripts/main-channel.mjs +452 -0
  226. package/scripts/mirror-odd-routing.mjs +2 -2
  227. package/scripts/npm-pack-result.mjs +31 -0
  228. package/scripts/render-cache-performance-worker.mjs +205 -0
  229. package/scripts/render-cache-performance.mjs +115 -0
  230. package/scripts/run-test-suite.mjs +90 -0
  231. package/scripts/test-installer-acceptance.sh +161 -0
  232. package/scripts/test-packed-runner.mjs +54 -19
  233. package/scripts/types-baseline.json +2 -92
  234. package/scripts/verify-package-files.mjs +91 -38
  235. package/skills/branch-pr/SKILL.md +24 -52
  236. package/skills/chained-pr/SKILL.md +31 -15
  237. package/skills/chained-pr/references/chaining-details.md +31 -20
  238. package/skills/gentle-ai/SKILL.md +16 -21
  239. package/skills/issue-creation/SKILL.md +8 -2
  240. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  241. package/skills/work-unit-commits/SKILL.md +4 -3
  242. package/tests/agent-home.test.ts +56 -3
  243. package/tests/agent-model-resolution.test.ts +113 -0
  244. package/tests/agent-profiles.test.ts +30 -0
  245. package/tests/agent-risk-escalation.test.ts +123 -0
  246. package/tests/agents-completion-delivery.test.ts +17 -0
  247. package/tests/agents-fake-child.ts +2 -2
  248. package/tests/agents-message-delivery.test.ts +106 -0
  249. package/tests/agents-protocol.test.ts +97 -0
  250. package/tests/agents-queries.test.ts +9 -9
  251. package/tests/agents-runner-process.test.ts +31 -0
  252. package/tests/agents-runner.test.ts +467 -93
  253. package/tests/agents-view-thread-identity.test.ts +169 -0
  254. package/tests/agents-view.test.ts +8 -2
  255. package/tests/agents-widget.test.ts +154 -15
  256. package/tests/append-system-prompt-route.test.ts +160 -0
  257. package/tests/append-system-prompt.test.ts +46 -0
  258. package/tests/artifact-language.test.ts +19 -213
  259. package/tests/ask-user-question.test.ts +44 -1
  260. package/tests/asset-installation-runtime.test.ts +5 -16
  261. package/tests/auto-mode-invariants.test.ts +258 -0
  262. package/tests/auto-mode-sdk.test.ts +57 -0
  263. package/tests/auto-mode.test.ts +49 -0
  264. package/tests/auto-permission-classifier.test.ts +283 -0
  265. package/tests/auto-permission-runtime.test.ts +196 -0
  266. package/tests/autonomous-guard.test.ts +69 -1
  267. package/tests/background-cache-warming.test.ts +2 -1
  268. package/tests/background-jobs.test.ts +227 -0
  269. package/tests/background-monitor.test.ts +195 -0
  270. package/tests/background-subagents-default-mode.test.ts +28 -5
  271. package/tests/bounded-stop-contract.test.ts +85 -0
  272. package/tests/bounded-writer-admission.test.ts +146 -0
  273. package/tests/branch-pr-skill.test.ts +43 -0
  274. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  275. package/tests/bridge-wake-rendering.test.ts +56 -0
  276. package/tests/build-distribution.test.ts +269 -0
  277. package/tests/builtin-codemode-optout.test.ts +242 -0
  278. package/tests/bundled-install.test.ts +1319 -0
  279. package/tests/bundled-wizard.test.ts +505 -0
  280. package/tests/card-content-policy.test.ts +68 -0
  281. package/tests/card-style-policy.test.ts +87 -0
  282. package/tests/chained-pr-skill.test.ts +124 -0
  283. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  284. package/tests/child-context-files.test.ts +255 -0
  285. package/tests/child-package-entrypoints.test.ts +65 -0
  286. package/tests/child-package-injection.test.ts +74 -0
  287. package/tests/child-safety.test.ts +94 -0
  288. package/tests/child-web-package.test.ts +224 -0
  289. package/tests/codegraph-tools.test.ts +35 -0
  290. package/tests/codemode-rendering.test.ts +596 -0
  291. package/tests/command-palette.test.ts +43 -7
  292. package/tests/context-threshold.test.ts +31 -0
  293. package/tests/delegate-for-reason-contract.test.ts +327 -0
  294. package/tests/delegated-key-learnings-contract.test.ts +3 -78
  295. package/tests/destructive-command-guard.test.ts +84 -0
  296. package/tests/devbinary/native-review-parity.devtest.ts +171 -3
  297. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  298. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  299. package/tests/engram-safety.test.ts +288 -0
  300. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  301. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  302. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  303. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  304. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  305. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  306. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  307. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  308. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  309. package/tests/generic-agent-tools.test.ts +69 -0
  310. package/tests/gentle-agents.test.ts +2804 -346
  311. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  312. package/tests/gentle-ai-binary.test.ts +21 -4
  313. package/tests/gentle-ai-child-guards.test.ts +202 -0
  314. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  315. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  316. package/tests/gentle-ai-installer.test.ts +566 -62
  317. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  318. package/tests/gentle-ai-renderer.test.ts +493 -14
  319. package/tests/gentle-ai.test.ts +1427 -236
  320. package/tests/gentle-card-text.ts +2 -1
  321. package/tests/gentle-herdr-activity.test.ts +100 -0
  322. package/tests/gentle-notifications.test.ts +427 -0
  323. package/tests/gentle-shell-bin.test.ts +896 -115
  324. package/tests/gentle-shell-bundled.test.ts +1002 -0
  325. package/tests/gentle-shell-launcher.test.ts +333 -65
  326. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  327. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  328. package/tests/gentle-shell-startup-update.test.ts +85 -0
  329. package/tests/gentle-shell-uninstall.test.ts +978 -0
  330. package/tests/gentle-shell-update-panel.test.ts +158 -0
  331. package/tests/gentle-shell.test.ts +4216 -119
  332. package/tests/gentle-stats.test.ts +152 -0
  333. package/tests/gentle-theme.test.ts +4 -1
  334. package/tests/gentle-todo.test.ts +333 -10
  335. package/tests/herdr-activity.test.ts +243 -0
  336. package/tests/history-agent-home.test.ts +81 -0
  337. package/tests/history-atomic-write.test.ts +57 -0
  338. package/tests/history-capture-policy.test.ts +102 -0
  339. package/tests/history-child-guard.test.ts +70 -0
  340. package/tests/history-command-registration.test.ts +164 -0
  341. package/tests/history-dedupe-entries.test.ts +123 -0
  342. package/tests/history-delete-backfill.test.ts +190 -0
  343. package/tests/history-delete-confirm.test.ts +460 -0
  344. package/tests/history-dispatch.test.ts +180 -0
  345. package/tests/history-drain-hidden.test.ts +110 -0
  346. package/tests/history-drain-order.test.ts +98 -0
  347. package/tests/history-expanded-globals.test.ts +62 -0
  348. package/tests/history-gc.test.ts +832 -0
  349. package/tests/history-header-layout.test.ts +265 -0
  350. package/tests/history-hide-prompts.test.ts +275 -0
  351. package/tests/history-lazy-windowing.test.ts +508 -0
  352. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  353. package/tests/history-load-shared-history.test.ts +53 -0
  354. package/tests/history-max-results-cap.test.ts +76 -0
  355. package/tests/history-multi-reader.test.ts +203 -0
  356. package/tests/history-off-path.test.ts +170 -0
  357. package/tests/history-openflow-integration.test.ts +173 -0
  358. package/tests/history-overlay-margin.test.ts +326 -0
  359. package/tests/history-preview-layout.test.ts +93 -0
  360. package/tests/history-registry.test.ts +143 -0
  361. package/tests/history-scope-delete.test.ts +411 -0
  362. package/tests/history-search-caret-keys.test.ts +142 -0
  363. package/tests/history-seed-bootstrap.test.ts +170 -0
  364. package/tests/history-seed-regen.test.ts +129 -0
  365. package/tests/history-selector-windowing.test.ts +94 -0
  366. package/tests/history-session-scan-directory.test.ts +87 -0
  367. package/tests/history-session-scan-extract.test.ts +583 -0
  368. package/tests/history-session-writer.test.ts +351 -0
  369. package/tests/history-store-paths.test.ts +79 -0
  370. package/tests/history-tombstone-exact.test.ts +139 -0
  371. package/tests/history-wheel-mouse.test.ts +242 -0
  372. package/tests/inprocess-reviewer.test.ts +49 -19
  373. package/tests/install-gentle-ai-failure.test.ts +69 -0
  374. package/tests/install-tui-mode-setting.test.ts +15 -0
  375. package/tests/install-wizard.test.ts +1223 -0
  376. package/tests/installer-bundles.test.ts +122 -0
  377. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  378. package/tests/installer-preflight.test.ts +750 -0
  379. package/tests/installer-probes.test.ts +1106 -0
  380. package/tests/installer-runner.test.ts +2158 -0
  381. package/tests/installer-server.test.ts +1567 -0
  382. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  383. package/tests/issue-creation-skill.test.ts +61 -0
  384. package/tests/jobs-view.test.ts +160 -0
  385. package/tests/main-channel-upgrade.test.ts +389 -0
  386. package/tests/main-channel.test.ts +344 -0
  387. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  388. package/tests/model-price-ratio.test.ts +116 -0
  389. package/tests/model-profiles-preservation.test.ts +153 -0
  390. package/tests/model-routing-authority.test.ts +16 -0
  391. package/tests/model-routing-package.test.ts +10 -0
  392. package/tests/nan-provider-runtime.test.ts +156 -0
  393. package/tests/nan-provider.test.ts +481 -0
  394. package/tests/native-binary-gate.test.ts +2 -0
  395. package/tests/native-review-capability-contract.test.ts +13 -1
  396. package/tests/native-review-cli.test.ts +239 -118
  397. package/tests/native-review-consent.test.ts +2 -2
  398. package/tests/native-review-parity-runtime.test.ts +101 -4
  399. package/tests/native-review-parity.test.ts +12 -48
  400. package/tests/native-risk-evidence-parity.test.ts +21 -0
  401. package/tests/no-self-pause-contract.test.ts +87 -0
  402. package/tests/notification-audio-native.test.ts +281 -0
  403. package/tests/notification-audio-windows.test.ts +581 -0
  404. package/tests/notification-audio.test.ts +548 -0
  405. package/tests/notification-customize.test.ts +655 -0
  406. package/tests/notification-events.test.ts +115 -0
  407. package/tests/notification-policy.test.ts +201 -0
  408. package/tests/notification-pulse-client.test.ts +356 -0
  409. package/tests/notification-pulse-protocol.test.ts +213 -0
  410. package/tests/notification-pulse-stream.test.ts +320 -0
  411. package/tests/notification-scheduler.test.ts +258 -0
  412. package/tests/notification-ui.test.ts +157 -0
  413. package/tests/npm-pack-result.test.ts +41 -0
  414. package/tests/odd-integration.test.ts +76 -0
  415. package/tests/odd-phase-inference.test.ts +213 -0
  416. package/tests/odd-phase-loader.test.ts +253 -0
  417. package/tests/odd-phase.test.ts +307 -0
  418. package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
  419. package/tests/odd-routing-contract.test.ts +158 -50
  420. package/tests/orchestrator-budget.test.ts +43 -45
  421. package/tests/orchestrator-catalog.test.ts +146 -0
  422. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  423. package/tests/orchestrator-consultation.test.ts +111 -0
  424. package/tests/orchestrator-discovery.test.ts +241 -0
  425. package/tests/orchestrator-helper-consent.test.ts +171 -0
  426. package/tests/orchestrator-helper.test.ts +297 -0
  427. package/tests/orchestrator-message-card.test.ts +190 -0
  428. package/tests/orchestrator-presence.test.ts +23 -0
  429. package/tests/orchestrator-rdd-ownership.test.ts +8 -7
  430. package/tests/orchestrator-scope.test.ts +107 -0
  431. package/tests/orchestrator-state.test.ts +114 -0
  432. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  433. package/tests/orchestrator-tool-card.test.ts +398 -0
  434. package/tests/orchestrator-work-search.test.ts +318 -0
  435. package/tests/orchestrator-work.test.ts +147 -0
  436. package/tests/overlay-repaint.test.ts +74 -0
  437. package/tests/package-manifest.test.ts +344 -123
  438. package/tests/packed-runner-owned-path.test.ts +80 -0
  439. package/tests/parallel-reason-contract.test.ts +21 -0
  440. package/tests/parallel-writer-admission.test.ts +210 -0
  441. package/tests/permission-mode-policy.test.ts +194 -0
  442. package/tests/persona-single-channel.test.ts +10 -18
  443. package/tests/pi-pretty.test.ts +56 -0
  444. package/tests/provider-defect-handoff.test.ts +7 -14
  445. package/tests/quiet-bash-runtime.test.ts +76 -0
  446. package/tests/quiet-tool-rendering.test.ts +569 -187
  447. package/tests/rdd-aware-verification-contract.test.ts +103 -40
  448. package/tests/rdd-status-line.test.ts +9 -4
  449. package/tests/render-cache-performance.test.ts +48 -0
  450. package/tests/resume-hint-extension.test.ts +122 -0
  451. package/tests/review-agent-end-preflight.test.ts +237 -21
  452. package/tests/review-candidate-owner-retry.test.ts +22 -1
  453. package/tests/review-candidate-view.test.ts +399 -12
  454. package/tests/review-contract-prompt.test.ts +138 -44
  455. package/tests/review-controller-lock-status.test.ts +0 -1
  456. package/tests/review-controller-native-recovery.test.ts +327 -17
  457. package/tests/review-controller-native-routing.test.ts +866 -6
  458. package/tests/review-controller-workspace-root.test.ts +206 -6
  459. package/tests/review-controller.test.ts +2 -2
  460. package/tests/review-correction-lifecycle.test.ts +2 -1
  461. package/tests/review-gate.test.ts +7 -4
  462. package/tests/review-graph-schema.test.ts +2 -2
  463. package/tests/review-host-relay-routing.test.ts +342 -4
  464. package/tests/review-host-relay.test.ts +49 -2
  465. package/tests/review-integration-v2-forward.test.ts +221 -1
  466. package/tests/review-json-arguments.test.ts +167 -0
  467. package/tests/review-last-event-closure.test.ts +157 -8
  468. package/tests/review-ledger-contract.test.ts +14 -37
  469. package/tests/review-policy-judgment-day.test.ts +1 -1
  470. package/tests/review-policy-ordinary.test.ts +2 -2
  471. package/tests/review-relay-transport-agent.test.ts +66 -1
  472. package/tests/review-reminder-receipt.test.ts +47 -1
  473. package/tests/review-risk-assessment.test.ts +594 -26
  474. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  475. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  476. package/tests/review-sidebar-state.test.ts +402 -0
  477. package/tests/review-test-fixtures.ts +7 -1
  478. package/tests/review-transaction.test.ts +18 -1
  479. package/tests/run-test-suite.test.ts +202 -0
  480. package/tests/runtime-harness.mjs +186 -800
  481. package/tests/runtime-metrics-children.test.ts +16 -23
  482. package/tests/runtime-metrics-extension.test.ts +1 -1
  483. package/tests/runtime-metrics-native.test.ts +31 -1
  484. package/tests/selection-engine.test.ts +451 -0
  485. package/tests/session-change-capture.test.ts +10 -10
  486. package/tests/session-changes-shell.test.ts +242 -48
  487. package/tests/session-changes.test.ts +17 -24
  488. package/tests/session-messaging-grants.test.ts +255 -0
  489. package/tests/session-profile-binding.test.ts +163 -0
  490. package/tests/session-profile-freeze.test.ts +381 -0
  491. package/tests/session-subject-routing-contract.test.ts +34 -0
  492. package/tests/session-worktree-registry.test.ts +134 -2
  493. package/tests/shell-bar.test.ts +422 -1
  494. package/tests/shell-card.test.ts +353 -1
  495. package/tests/shell-changes-view.test.ts +135 -32
  496. package/tests/shell-changes.test.ts +61 -62
  497. package/tests/shell-prompt.test.ts +94 -2
  498. package/tests/shell-render-cache-runtime.test.ts +69 -0
  499. package/tests/shell-render-cache.test.ts +129 -0
  500. package/tests/shell-sidebar-layout.test.ts +351 -22
  501. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  502. package/tests/shell-todo.test.ts +207 -3
  503. package/tests/shell-usage-view.test.ts +28 -1
  504. package/tests/shell-usage.test.ts +108 -0
  505. package/tests/short-check-routing-contract.test.ts +49 -0
  506. package/tests/skill-registry-watcher.test.ts +119 -0
  507. package/tests/skill-registry.test.ts +357 -2
  508. package/tests/startup-banner.test.ts +246 -4
  509. package/tests/startup-home-repo.test.ts +170 -0
  510. package/tests/stats-collector.test.ts +195 -0
  511. package/tests/stats-view.test.ts +202 -0
  512. package/tests/status-timing-diagnostics.test.ts +283 -0
  513. package/tests/support/orchestrator-modules.ts +19 -0
  514. package/tests/task-size-routing-contract.test.ts +241 -0
  515. package/tests/telemetry-trigger.test.ts +107 -20
  516. package/tests/test-strength-contract.test.ts +124 -0
  517. package/tests/theme-customization.test.ts +72 -0
  518. package/tests/verify-agent-contract.test.ts +90 -0
  519. package/tests/verify-first-launch-contract.test.ts +139 -0
  520. package/tests/verify-package-files.test.ts +56 -0
  521. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  522. package/tests/vim-editor-adapter.test.ts +804 -0
  523. package/tests/vim-normal-engine.test.ts +101 -0
  524. package/tests/vim-operator-engine.test.ts +215 -0
  525. package/tests/vim-policy.test.ts +19 -0
  526. package/tests/vim-visual-engine.test.ts +52 -0
  527. package/tests/visual-customization-policy.test.ts +139 -0
  528. package/tests/visual-customize-view.test.ts +696 -0
  529. package/tests/visual-profiles.test.ts +87 -0
  530. package/tests/writer-edit-surface-scope.test.ts +59 -2
  531. package/tests/writer-surfaces.test.ts +199 -0
  532. package/tests/yolo-customize.test.ts +271 -0
  533. package/tests/yolo-mode-runtime.test.ts +188 -0
  534. package/tests/yolo-mode.test.ts +289 -0
  535. package/tests/yolo-session-policy.test.ts +59 -0
  536. package/themes/Gentle.json +2 -1
  537. package/themes/Gentleman-Cute.json +2 -1
  538. package/themes/Gentleman-Sexy.json +2 -1
  539. package/assets/agents/sdd-apply.md +0 -159
  540. package/assets/agents/sdd-archive.md +0 -228
  541. package/assets/agents/sdd-design.md +0 -49
  542. package/assets/agents/sdd-explore.md +0 -48
  543. package/assets/agents/sdd-init.md +0 -56
  544. package/assets/agents/sdd-onboard.md +0 -52
  545. package/assets/agents/sdd-proposal.md +0 -64
  546. package/assets/agents/sdd-remediate.md +0 -37
  547. package/assets/agents/sdd-research.md +0 -49
  548. package/assets/agents/sdd-spec.md +0 -192
  549. package/assets/agents/sdd-status.md +0 -54
  550. package/assets/agents/sdd-tasks.md +0 -108
  551. package/assets/agents/sdd-verify.md +0 -124
  552. package/assets/chains/sdd-full.chain.md +0 -83
  553. package/assets/chains/sdd-plan.chain.md +0 -56
  554. package/assets/chains/sdd-verify.chain.md +0 -43
  555. package/assets/sdd-orchestrator-workflow.md +0 -319
  556. package/assets/support/sdd-status-contract.md +0 -77
  557. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  558. package/extensions/sdd-init.ts +0 -816
  559. package/lib/openspec-deltas.ts +0 -156
  560. package/lib/sdd-preflight.ts +0 -1066
  561. package/lib/sdd-research-capabilities.ts +0 -94
  562. package/lib/sdd-status.ts +0 -26
  563. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  564. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  565. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  566. package/tests/openspec-deltas.test.ts +0 -209
  567. package/tests/sdd-agent-tools.test.ts +0 -156
  568. package/tests/sdd-archive-replay.test.ts +0 -82
  569. package/tests/sdd-classical-continuation.test.ts +0 -74
  570. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  571. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  572. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  573. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  574. package/tests/sdd-odd-integration.test.ts +0 -33
  575. package/tests/sdd-optional-research.test.ts +0 -124
  576. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  577. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  578. package/tests/sdd-preflight.test.ts +0 -541
  579. package/tests/sdd-research-capabilities.test.ts +0 -114
  580. package/tests/sdd-research-live.test.ts +0 -241
  581. package/tests/sdd-selection-transport.test.ts +0 -653
  582. package/tests/sdd-status.test.ts +0 -9
  583. package/tests/sdd-task-truth.test.ts +0 -43
  584. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  585. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  586. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  587. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  588. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  589. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  590. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,205 @@
1
+ import assert from "node:assert/strict";
2
+ import { createHash } from "node:crypto";
3
+ import { readFileSync } from "node:fs";
4
+ import { resolve } from "node:path";
5
+ import { performance } from "node:perf_hooks";
6
+ import { pathToFileURL } from "node:url";
7
+
8
+ const config = JSON.parse(process.argv[2]);
9
+ const piVersion = JSON.parse(readFileSync(resolve(config.piRoot, "package.json"), "utf8")).version;
10
+ const tuiVersion = JSON.parse(readFileSync(resolve(config.piRoot, "node_modules/@earendil-works/pi-tui/package.json"), "utf8")).version;
11
+ assert.equal(piVersion, "1.1.0", "private frame API audited only on Pi 1.1.0");
12
+ assert.equal(tuiVersion, piVersion, "host TUI package must match");
13
+ const hostImport = (path) => import(pathToFileURL(resolve(config.piRoot, "dist", path)).href);
14
+ const { createJiti } = await hostImport("core/extensions/jiti-loader.js");
15
+ const { VIRTUAL_MODULES } = await hostImport("core/extensions/virtual-modules.js");
16
+ const pi = VIRTUAL_MODULES["@earendil-works/pi-coding-agent"];
17
+ const { Container, TuiAltScreen } = VIRTUAL_MODULES["@earendil-works/pi-tui"];
18
+ const nativeTheme = await hostImport("modes/interactive/theme/theme.js");
19
+ const { KeybindingsManager } = await hostImport("core/keybindings.js");
20
+ const { ToolExecutionComponent } = await hostImport("modes/interactive/components/tool-execution.js");
21
+ const { UserMessageComponent } = await hostImport("modes/interactive/components/user-message.js");
22
+ const { AssistantMessageComponent } = await hostImport("modes/interactive/components/assistant-message.js");
23
+ const { createChatViewport } = await hostImport("modes/interactive/chat-viewport.js");
24
+ const loader = createJiti(import.meta.url, { moduleCache: false, fsCache: false, virtualModules: VIRTUAL_MODULES, tryNative: false });
25
+ const source = (path) => loader.import(resolve(config.root, path));
26
+ const shell = await source("extensions/gentle-shell.ts");
27
+ const quiet = await source("extensions/quiet-tools.ts");
28
+ const bar = await source("lib/shell-bar.ts");
29
+ const sidebar = await source("lib/shell-sidebar.ts");
30
+ const layout = await source("lib/shell-sidebar-layout.ts");
31
+ const cards = await source("lib/shell-card.ts");
32
+ assert.equal(Object.getPrototypeOf(shell.GentlePromptEditor.prototype), pi.CustomEditor.prototype, "editor must extend this exact host class");
33
+ const memory = () => Object.fromEntries(Object.entries(process.memoryUsage()).map(([key, bytes]) => [key, bytes / 1024 / 1024]));
34
+ const digest = (lines) => createHash("sha256").update(lines.join("\n")).digest("hex");
35
+ const usage = { input: 100, output: 20, cacheRead: 0, cacheWrite: 0, totalTokens: 120, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0.001 } };
36
+
37
+ function fixture(spec, live = false) {
38
+ pi.initTheme("dark");
39
+ cards.setCardStyle(spec.style);
40
+ const counts = { paints: 0, projections: 0, entries: 0, requests: 0, pulses: 0, bytes: 0 };
41
+ const theme = { fg: (...args) => nativeTheme.theme.fg(...args), bg: (...args) => nativeTheme.theme.bg(...args), bold: (text) => nativeTheme.theme.bold(text) };
42
+ const toolTheme = { ...theme, fg: (...args) => { counts.paints++; return theme.fg(...args); }, bg: (...args) => { counts.paints++; return theme.bg(...args); } };
43
+ const geometry = { columns: spec.width, rows: 32 };
44
+ let inputHandler;
45
+ const terminal = {
46
+ start(onInput) { inputHandler = onInput; }, stop() {}, async drainInput() {}, write(text) { counts.bytes += Buffer.byteLength(text); },
47
+ get columns() { return geometry.columns; }, get rows() { return geometry.rows; },
48
+ moveBy() {}, hideCursor() {}, showCursor() {}, clearLine() {}, clearFromCursor() {}, clearScreen() {},
49
+ setTitle() {}, setProgress() {}, kittyProtocolActive: false, getBufferedInput() { return ""; }, hasPendingInput() { return false; },
50
+ };
51
+ const tui = new TuiAltScreen(terminal, false);
52
+ const manager = pi.SessionManager.inMemory("/performance", { id: "performance-session" });
53
+ const getEntries = manager.getEntries.bind(manager);
54
+ manager.getEntries = () => { counts.entries++; return getEntries(); };
55
+ const model = { id: "synthetic-model", provider: "synthetic", reasoning: true, contextWindow: 200000 };
56
+ const ctx = { model, sessionManager: manager, modelRegistry: { isUsingOAuth: () => false },
57
+ getContextUsage() { counts.projections++; return pi.AgentSession.prototype.getContextUsage.call({ sessionManager: manager, _limitsModel: () => model }); } };
58
+ const api = { getThinkingLevel: () => "high" };
59
+ const footerData = { getGitBranch: () => "performance", getExtensionStatuses: () => new Map(), onBranchChange: () => () => {} };
60
+ let clock = 0;
61
+ const editor = new shell.GentlePromptEditor(tui, nativeTheme.getEditorTheme(), new KeybindingsManager(), {
62
+ ...theme, pending: () => false, now: () => live ? Date.now() : clock, doubleEscCancelEnabled: () => false,
63
+ dispatchQueuedText() {}, requestRender() { counts.requests++; if (live) tui.requestRender(); },
64
+ });
65
+ let pulse = () => {};
66
+ if (live) editor.setWorking(true);
67
+ else {
68
+ // Capture the actual quality interval callback, not a reimplementation.
69
+ const original = globalThis.setInterval;
70
+ try { globalThis.setInterval = (fn, ms) => { assert.equal(ms, 80); pulse = () => { counts.pulses++; fn(); }; return { unref() {} }; }; editor.setWorking(true); }
71
+ finally { globalThis.setInterval = original; }
72
+ }
73
+ const document = new Container();
74
+ const renderer = quiet.createQuietToolRenderer("read");
75
+ const definition = { ...renderer, renderCall: (args, _theme, context) => renderer.renderCall(args, toolTheme, context),
76
+ renderResult: (result, options, _theme, context) => renderer.renderResult(result, options, toolTheme, context) };
77
+ let toolCount = 0;
78
+ let latest;
79
+ const append = () => {
80
+ const id = `tool-${toolCount++}`;
81
+ const args = { path: `src/file-${id}.ts` };
82
+ const text = `Inspect **${id}** with Unicode 広い and ANSI-safe content.`;
83
+ manager.appendMessage({ role: "user", content: text, timestamp: 1 });
84
+ const assistant = { role: "assistant", content: [{ type: "text", text: "Checking `source` and a **settled** paragraph." }, { type: "toolCall", id, name: "read", arguments: args }],
85
+ api: "openai-responses", provider: "synthetic", model: model.id, stopReason: "toolUse", timestamp: 1, usage };
86
+ manager.appendMessage(assistant);
87
+ const result = { content: [{ type: "text", text: `Result ${id}\nconst answer = 42;\n${"long wrapped output ".repeat(12)}` }], details: {}, isError: false };
88
+ manager.appendMessage({ ...result, role: "toolResult", toolCallId: id, toolName: "read", timestamp: 1 });
89
+ document.addChild(new UserMessageComponent(text));
90
+ document.addChild(new AssistantMessageComponent(assistant));
91
+ latest = new ToolExecutionComponent("read", id, args, {}, definition, tui, "/performance");
92
+ latest.setArgsComplete(); latest.updateResult(result); document.addChild(latest);
93
+ };
94
+ for (let index = 0; index < spec.tools; index++) append();
95
+ const footer = new Container();
96
+ const modelForFrame = () => shell.buildShellBarModel(api, ctx, footerData);
97
+ const bottom = shell.createShellBarComponent(api, ctx, tui, theme, footerData);
98
+ footer.addChild(sidebar.sidebarPart(tui, "footer", bottom, {
99
+ digest: () => JSON.stringify(modelForFrame()), render: (width) => bar.renderShellSidebarBar(modelForFrame(), theme, width), invalidate() {},
100
+ }));
101
+ const disposeHeader = sidebar.sidebarHeader(tui, { digest: () => JSON.stringify(modelForFrame()),
102
+ render: (width) => bar.renderShellHeaderChrome(bar.buildShellHeaderModel(modelForFrame()), theme, width).rows, invalidate() {} });
103
+ const viewport = createChatViewport({ document, pendingMessages: new Container(), status: new Container(), widgetsAbove: new Container(),
104
+ editor, widgetsBelow: new Container(), footer, scrollbar: "hidden" });
105
+ tui.setLayoutRoot(viewport.root); tui.setFocus(editor);
106
+ let uninstall = layout.installSidebar(tui, theme);
107
+ tui.start();
108
+ const render = () => {
109
+ tui.doRender();
110
+ assert.equal(sidebar.sidebarState(tui).active, geometry.columns >= layout.SIDEBAR_BREAKPOINT, "sidebar must really own wide frames");
111
+ assert.equal(cards.cardStyle(), spec.style);
112
+ return [...tui.previousScreen];
113
+ };
114
+ return { tui, editor, manager, counts, render, input: (data) => inputHandler(data),
115
+ advance() { clock += 80; pulse(); },
116
+ apply(scenario, index) {
117
+ if (scenario === "scroll") { if (index % 2) viewport.transcript.scrollToEnd(); else viewport.transcript.scrollTo(0, { disableFollow: true }); }
118
+ if (scenario === "typing") { const text = editor.getText(); editor.handleInput("x"); assert.equal(editor.getText(), text + "x"); }
119
+ if (scenario === "stream") latest.updateResult({ content: [{ type: "text", text: `stream ${index} ${"partial ".repeat(index + 1)}` }], details: {} }, true);
120
+ if (scenario === "append") append();
121
+ if (scenario === "resize") { const previous = geometry.columns; geometry.columns = index % 2 ? spec.width : Math.max(40, spec.width - 60); assert.notEqual(geometry.columns, previous); }
122
+ if (scenario === "theme") {
123
+ const previous = theme.bg("toolSuccessBg", "");
124
+ pi.initTheme(index % 2 ? "dark" : "light");
125
+ assert.notEqual(theme.bg("toolSuccessBg", ""), previous);
126
+ document.invalidate(); editor.invalidate();
127
+ uninstall(); uninstall = layout.installSidebar(tui, { ...theme });
128
+ }
129
+ },
130
+ close() { editor.dispose(); disposeHeader(); uninstall(); bottom.dispose(); tui.stop(); },
131
+ };
132
+ }
133
+
134
+ function controlled(spec) {
135
+ globalThis.gc();
136
+ const idleMemory = memory();
137
+ const buildStart = performance.now();
138
+ const scene = fixture(spec);
139
+ const buildMs = performance.now() - buildStart;
140
+ assert.equal(scene.counts.projections, 0, "first frame must not already have rendered");
141
+ const coldStart = performance.now();
142
+ const coldLines = scene.render();
143
+ const coldMs = performance.now() - coldStart;
144
+ const coldScreen = digest(coldLines);
145
+ for (let index = 0; index < config.warmup; index++) { scene.advance(); scene.render(); }
146
+ globalThis.gc();
147
+ const warmMemory = memory();
148
+ const initial = { ...scene.counts };
149
+ const result = { spec, buildMs, coldMs, coldScreen, idleMemory, warmMemory, wallMs: [], cpuMs: [], screens: [] };
150
+ const samples = ["resize", "theme"].includes(spec.scenario) ? Math.min(8, config.samples) : config.samples;
151
+ try {
152
+ for (let index = 0; index < samples; index++) {
153
+ const cpu = process.cpuUsage();
154
+ const start = performance.now();
155
+ scene.advance(); scene.apply(spec.scenario, index);
156
+ const screen = scene.render();
157
+ result.wallMs.push(performance.now() - start);
158
+ const used = process.cpuUsage(cpu);
159
+ result.cpuMs.push((used.user + used.system) / 1000);
160
+ result.screens.push(digest(screen));
161
+ }
162
+ result.counters = Object.fromEntries(Object.entries(scene.counts).map(([key, value]) => [key, value - initial[key]]));
163
+ assert.equal(result.counters.pulses, samples, "every controlled frame advances the real quality pulse");
164
+ } finally { scene.close(); }
165
+ return result;
166
+ }
167
+
168
+ async function live(spec) {
169
+ const scene = fixture(spec, true);
170
+ for (let index = 0; index < config.warmup; index++) scene.render();
171
+ scene.editor.setWorking(false); scene.editor.setWorking(true); // Align real pulse onset after warmup.
172
+ const original = scene.tui.doRender.bind(scene.tui);
173
+ const result = { spec, durationMs: config.liveMs, frameMs: [], inputToFrameMs: [], inputTimerDriftMs: [] };
174
+ const started = performance.now();
175
+ const initial = { ...scene.counts };
176
+ let pendingInput;
177
+ let expected = started + 100;
178
+ scene.tui.doRender = () => {
179
+ const start = performance.now(); original();
180
+ result.frameMs.push(performance.now() - start);
181
+ if (pendingInput !== undefined) { result.inputToFrameMs.push(performance.now() - pendingInput); pendingInput = undefined; }
182
+ };
183
+ const input = setInterval(() => {
184
+ result.inputTimerDriftMs.push(performance.now() - expected); expected += 100;
185
+ pendingInput ??= performance.now(); scene.input("x");
186
+ }, 100);
187
+ try { await new Promise((resolve) => setTimeout(resolve, config.liveMs)); }
188
+ finally { clearInterval(input); scene.close(); }
189
+ result.actualDurationMs = performance.now() - started;
190
+ result.counters = Object.fromEntries(Object.entries(scene.counts).map(([key, value]) => [key, value - initial[key]]));
191
+ return result;
192
+ }
193
+
194
+ const cases = [];
195
+ for (const tools of config.counts) for (const width of config.widths) for (const style of config.styles) {
196
+ for (const scenario of ["pulse", "scroll", "typing", "stream", "append", "resize", "theme"]) {
197
+ const result = controlled({ tools, width, style, scenario });
198
+ // WeakRef keep-alive lasts until the job boundary; collect only after it.
199
+ await new Promise((resolve) => setImmediate(resolve)); globalThis.gc(); result.closedMemory = memory(); cases.push(result);
200
+ }
201
+ }
202
+ const liveRuns = config.liveMs ? [await live({ tools: Math.max(...config.counts), width: Math.max(...config.widths), style: config.styles[0], scenario: "real-quality-timers-and-typing" })] : [];
203
+ const metadata = { piVersion, tuiVersion,
204
+ editorIdentityVerified: true, animation: "quality/80ms", terminal: "in-memory ANSI sink; no emulator", scheduler: "controlled quality callback; separate real-timer run" };
205
+ console.log(JSON.stringify({ metadata, cases, live: liveRuns }));
@@ -0,0 +1,115 @@
1
+ // Diagnostic comparison, not a speed gate. Run each source in a fresh process.
2
+ import assert from "node:assert/strict";
3
+ import { execFileSync } from "node:child_process";
4
+ import { createHash } from "node:crypto";
5
+ import { closeSync, ftruncateSync, openSync, readFileSync, readdirSync, realpathSync, writeSync } from "node:fs";
6
+ import { cpus, loadavg } from "node:os";
7
+ import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
8
+ import { fileURLToPath } from "node:url";
9
+
10
+ export function percentile(values, fraction) {
11
+ if (!values.length) return null;
12
+ const sorted = [...values].sort((a, b) => a - b);
13
+ const position = (sorted.length - 1) * fraction;
14
+ const low = Math.floor(position);
15
+ return sorted[low] + (sorted[Math.ceil(position)] - sorted[low]) * (position - low);
16
+ }
17
+ export function summarize(values) {
18
+ return { count: values.length, median: percentile(values, 0.5), p95: percentile(values, 0.95), max: values.length ? Math.max(...values) : null };
19
+ }
20
+ export function compare(before, after) {
21
+ assert.deepEqual(after.metadata, before.metadata, "host and scheduler must match");
22
+ assert.equal(before.cases.length, after.cases.length);
23
+ return before.cases.map((original, index) => {
24
+ const candidate = after.cases[index];
25
+ assert.deepEqual(candidate.spec, original.spec);
26
+ assert.equal(candidate.coldScreen, original.coldScreen, "cold visible output differs");
27
+ assert.deepEqual(candidate.screens, original.screens, `visible output differs: ${JSON.stringify(original.spec)}`);
28
+ return { spec: original.spec, before: summarize(original.wallMs), after: summarize(candidate.wallMs),
29
+ speedup: percentile(original.wallMs, 0.5) / percentile(candidate.wallMs, 0.5) };
30
+ });
31
+ }
32
+ function hashFiles(root, paths) {
33
+ const hash = createHash("sha256");
34
+ for (const path of paths.sort()) hash.update(path + "\0").update(readFileSync(resolve(root, path))).update("\0");
35
+ return hash.digest("hex");
36
+ }
37
+ function fingerprint(root) {
38
+ const paths = execFileSync("git", ["-C", root, "ls-files", "-z"], { encoding: "utf8" }).split("\0").filter(Boolean);
39
+ return { root, head: execFileSync("git", ["-C", root, "rev-parse", "HEAD"], { encoding: "utf8" }).trim(), sourceSha256: hashFiles(root, paths) };
40
+ }
41
+ function hostFingerprint(root) {
42
+ const packages = [root, ...["pi-tui", "pi-ai", "pi-agent-core", "jiti"].map((name) => resolve(root, "node_modules", name === "jiti" ? name : `@earendil-works/${name}`))];
43
+ return packages.map((path) => {
44
+ const packageRoot = realpathSync(path);
45
+ const files = readdirSync(packageRoot, { recursive: true, withFileTypes: true }).filter((entry) => entry.isFile()).map((entry) => relative(packageRoot, resolve(entry.parentPath, entry.name)));
46
+ return { root: packageRoot, package: JSON.parse(readFileSync(resolve(path, "package.json"), "utf8")).version, sha256: hashFiles(packageRoot, files) };
47
+ });
48
+ }
49
+ export function outputDescriptor(output, protectedRoots) {
50
+ const canonical = resolve(realpathSync(dirname(resolve(output))), basename(output));
51
+ for (const root of protectedRoots) {
52
+ const path = relative(realpathSync(root), canonical);
53
+ assert.ok(path && (isAbsolute(path) || path === ".." || path.startsWith(".." + sep)), "output must be outside source, harness and host roots");
54
+ }
55
+ return openSync(canonical, "wx", 0o600);
56
+ }
57
+ async function main() {
58
+ const options = Object.fromEntries(process.argv.slice(2).map((arg) => {
59
+ assert.match(arg, /^--[a-z-]+=.+$/, "use --name=value arguments");
60
+ const split = arg.indexOf("="); return [arg.slice(2, split), arg.slice(split + 1)];
61
+ }));
62
+ for (const key of ["baseline-root", "candidate-root", "pi-root", "output"]) assert.ok(options[key], `--${key} is required`);
63
+ const roots = { before: resolve(options["baseline-root"]), after: resolve(options["candidate-root"]) };
64
+ const initial = Object.fromEntries(Object.entries(roots).map(([key, root]) => [key, fingerprint(root)]));
65
+ assert.equal(initial.before.head, initial.after.head, "compare worktrees based on the same commit");
66
+ const numeric = (name, fallback, min) => { const value = Number(options[name] ?? fallback); assert.ok(Number.isInteger(value) && value >= min, `invalid --${name}`); return value; };
67
+ const list = (name, fallback) => (options[name] ?? fallback).split(",").map(Number);
68
+ const config = { piRoot: resolve(options["pi-root"]), counts: list("counts", "100,1000,5000"), widths: list("widths", "100,160"),
69
+ styles: (options.styles ?? "float,neon").split(","), samples: numeric("samples", 50, 2), warmup: 5, liveMs: numeric("live-ms", 6000, 0) };
70
+ assert.ok([...config.counts, ...config.widths].every((value) => Number.isInteger(value) && value > 0));
71
+ assert.ok(config.styles.length <= 2 && config.styles.every((style) => ["float", "neon"].includes(style)));
72
+ assert.ok(config.widths.every((width) => width > 40), "resize scenarios need widths above 40");
73
+ const repetitions = numeric("repetitions", 4, 1);
74
+ assert.ok(repetitions <= 20 && config.samples <= 2000 && config.liveMs <= 60000);
75
+ assert.ok(config.counts.length <= 4 && config.widths.length <= 4 && Math.max(...config.counts) <= 20000 && Math.max(...config.widths) <= 300);
76
+ const report = { schema: "gentle-shell.render-performance/v1", started: new Date().toISOString(), node: process.version,
77
+ cpu: cpus()[0]?.model, loadAtStart: loadavg(), sources: initial, config, runs: [], comparisons: [] };
78
+ const worker = fileURLToPath(new URL("render-cache-performance-worker.mjs", import.meta.url));
79
+ const instrumentationHash = () => createHash("sha256").update(readFileSync(fileURLToPath(import.meta.url))).update(readFileSync(worker)).digest("hex");
80
+ report.instrumentationSha256 = instrumentationHash();
81
+ report.host = hostFingerprint(config.piRoot);
82
+ const fd = outputDescriptor(options.output, [...Object.values(roots), config.piRoot, ...report.host.map((entry) => entry.root), resolve(dirname(fileURLToPath(import.meta.url)), "..")]);
83
+ const persist = () => {
84
+ const data = Buffer.from(JSON.stringify(report, null, 2) + "\n");
85
+ ftruncateSync(fd, 0);
86
+ let offset = 0;
87
+ while (offset < data.length) {
88
+ const written = writeSync(fd, data, offset, data.length - offset, offset);
89
+ assert.ok(written > 0); offset += written;
90
+ }
91
+ };
92
+ try {
93
+ for (let repetition = 0; repetition < repetitions; repetition++) {
94
+ const pair = {};
95
+ for (const variant of repetition % 2 ? ["after", "before"] : ["before", "after"]) {
96
+ console.log(`replica ${repetition + 1}/${repetitions}: ${variant}`);
97
+ const output = execFileSync(process.execPath, ["--expose-gc", "--experimental-strip-types", worker,
98
+ JSON.stringify({ ...config, root: roots[variant], repetition })], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024, timeout: 1_200_000 });
99
+ pair[variant] = JSON.parse(output);
100
+ report.runs.push({ repetition, variant, ...pair[variant] });
101
+ persist();
102
+ }
103
+ report.comparisons.push({ repetition, cases: compare(pair.before, pair.after) });
104
+ persist();
105
+ }
106
+ for (const [key, root] of Object.entries(roots)) assert.deepEqual(fingerprint(root), initial[key], "source changed during measurement");
107
+ assert.equal(instrumentationHash(), report.instrumentationSha256, "instrumentation changed during measurement");
108
+ assert.deepEqual(hostFingerprint(config.piRoot), report.host, "host runtime changed during measurement");
109
+ report.finished = new Date().toISOString();
110
+ report.loadAtEnd = loadavg();
111
+ persist();
112
+ } finally { closeSync(fd); }
113
+ console.log(`PASS: ${report.comparisons[0].cases.length} equivalent cases × ${repetitions} paired replicas; raw report: ${resolve(options.output)}`);
114
+ }
115
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await main();
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // Sequential test-suite runner for `pnpm test` (gentle-pi#1285).
3
+ //
4
+ // `pnpm test` used to chain its three stages with `&&`, so any stage-1
5
+ // failure — including a known-flaky unit test — silently suppressed the
6
+ // provider-contract check and the runtime harness while reporting a plain
7
+ // "test failed". This runner executes every stage unconditionally, prints a
8
+ // per-stage header and a final summary, and exits non-zero when any stage
9
+ // fails. Stages are plain shell commands, so behavior stays identical on
10
+ // POSIX and Windows CI.
11
+
12
+ import { spawn } from "node:child_process";
13
+ import { mkdtempSync, readFileSync, realpathSync, rmSync } from "node:fs";
14
+ import { tmpdir } from "node:os";
15
+ import { join } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+
18
+ export const DEFAULT_STAGES = Object.freeze([
19
+ { name: "unit-tests", command: "node --experimental-strip-types --test tests/*.test.ts" },
20
+ { name: "provider-contract", command: "pnpm run check:provider-contract" },
21
+ { name: "runtime-harness", command: "pnpm run test:harness" },
22
+ ]);
23
+
24
+ export async function runStage(stage) {
25
+ // Unit fixtures must not inherit the terminal's Git overrides, child-session
26
+ // identity, or personal guardrails. Other stages retain their existing env.
27
+ const configHome = stage.name === "unit-tests" ? mkdtempSync(join(tmpdir(), "gentle-pi-unit-config-")) : undefined;
28
+ const env = configHome ? Object.fromEntries(Object.entries(process.env).filter(([key]) => {
29
+ const normalizedKey = key.toUpperCase();
30
+ return !normalizedKey.startsWith("GIT_") && !normalizedKey.startsWith("GENTLE_PI_AGENTS_") && normalizedKey !== "GENTLE_PI_CONFIG_HOME";
31
+ })) : process.env;
32
+ if (configHome) env.GENTLE_PI_CONFIG_HOME = configHome;
33
+ try {
34
+ return await new Promise((resolve) => {
35
+ const child = spawn(stage.command, { shell: true, stdio: "inherit", env });
36
+ child.on("close", (code) => resolve({ name: stage.name, code: code ?? 1 }));
37
+ child.on("error", (error) => {
38
+ console.error(`\n[${stage.name}] spawn failed: ${error.message}`);
39
+ resolve({ name: stage.name, code: 1 });
40
+ });
41
+ });
42
+ } finally {
43
+ if (configHome) rmSync(configHome, { recursive: true, force: true });
44
+ }
45
+ }
46
+
47
+ export async function runTestSuite(stages = DEFAULT_STAGES, { runStageImpl = runStage, write = (line) => console.log(line) } = {}) {
48
+ const results = [];
49
+ for (const stage of stages) {
50
+ write(`\n=== [${stage.name}] ${stage.command} ===`);
51
+ results.push(await runStageImpl(stage));
52
+ }
53
+ write("\n=== test suite summary ===");
54
+ for (const result of results) write(`${result.code === 0 ? "PASS" : "FAIL"} ${result.name}`);
55
+ const failures = results.filter((result) => result.code !== 0);
56
+ write(failures.length === 0 ? "all stages passed" : `${failures.length} stage(s) failed: ${failures.map((f) => f.name).join(", ")}`);
57
+ return results;
58
+ }
59
+
60
+ function readStagesFromJsonPath(jsonPath) {
61
+ const stages = JSON.parse(readFileSync(jsonPath, "utf8"));
62
+ if (!Array.isArray(stages) || stages.some((stage) => typeof stage?.name !== "string" || typeof stage?.command !== "string")) {
63
+ throw new Error(`stages file must be an array of { name, command } objects: ${jsonPath}`);
64
+ }
65
+ return stages;
66
+ }
67
+
68
+ // Direct-invocation detection must survive symlinks: Node real-paths
69
+ // `import.meta.url` but keeps `process.argv[1]` logical, so a plain equality
70
+ // check silently no-ops the whole runner (zero stages run, exit 0) when the
71
+ // script is reached through a file symlink.
72
+ function isDirectRun(argv1) {
73
+ if (!argv1) return false;
74
+ try {
75
+ return realpathSync(argv1) === realpathSync(fileURLToPath(import.meta.url));
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ if (isDirectRun(process.argv[1])) {
82
+ try {
83
+ const stages = process.argv[2] ? readStagesFromJsonPath(process.argv[2]) : DEFAULT_STAGES;
84
+ const results = await runTestSuite(stages);
85
+ process.exitCode = results.some((result) => result.code !== 0) ? 1 : 0;
86
+ } catch (error) {
87
+ console.error(error instanceof Error ? error.message : error);
88
+ process.exitCode = 1;
89
+ }
90
+ }
@@ -0,0 +1,161 @@
1
+ #!/bin/sh
2
+ # Opt-in Linux clean-machine acceptance for the browser installation wizard.
3
+ # Runs the real bootstrap and wizard inside a disposable debian:bookworm-slim
4
+ # container as a non-root user. Network access happens only inside that
5
+ # container; nothing reads or writes the host HOME. Not part of CI.
6
+ #
7
+ # Usage: sh scripts/test-installer-acceptance.sh [--worktree]
8
+ # default bundle = `git archive HEAD`
9
+ # --worktree bundle = tracked files as they are in the working tree,
10
+ # including uncommitted modifications (untracked files excluded)
11
+ # GENTLE_ACCEPTANCE_KEEP=1 keeps a failed run's container for inspection.
12
+ set -eu
13
+ fail() { printf 'acceptance: %s\n' "$1" >&2; exit 1; }
14
+ note() { printf 'acceptance: %s\n' "$1"; }
15
+ source_mode='head'
16
+ case $# in
17
+ 0) ;;
18
+ 1) [ "$1" = --worktree ] || fail "unknown argument: $1"; source_mode=worktree;;
19
+ *) fail 'usage: sh scripts/test-installer-acceptance.sh [--worktree]';;
20
+ esac
21
+ image=debian:bookworm-slim
22
+ # Upper bound for the whole installation inside the container.
23
+ install_limit=${GENTLE_ACCEPTANCE_INSTALL_SECONDS:-1500}
24
+ case $install_limit in ''|*[!0-9]*) fail 'GENTLE_ACCEPTANCE_INSTALL_SECONDS must be a whole number';; esac
25
+ command -v docker >/dev/null 2>&1 || fail 'Docker is required; refusing to run anything on the host'
26
+ docker info >/dev/null 2>&1 || fail 'Docker daemon is unavailable; refusing to run anything on the host'
27
+ command -v git >/dev/null 2>&1 || fail 'git is required to package the bundle'
28
+ repo=$(git rev-parse --show-toplevel) || fail 'run from inside the repository'
29
+ started=$(date +%s)
30
+ elapsed() { printf '%ss' "$(( $(date +%s) - started ))"; }
31
+ name=gentle-install-acceptance-$$-$started
32
+ container=
33
+ passed=0
34
+ cleanup() {
35
+ if [ -n "$container" ] && [ "$passed" = 0 ] && [ "${GENTLE_ACCEPTANCE_KEEP:-}" = 1 ]; then
36
+ printf 'acceptance: kept %s; inspect with: docker exec -it -u tester %s bash; remove with: docker rm -f %s\n' "$container" "$container" "$container" >&2
37
+ elif [ -n "$container" ]; then
38
+ docker rm -f "$container" >/dev/null 2>&1 || printf 'acceptance: could not remove container %s\n' "$container" >&2
39
+ fi
40
+ }
41
+ trap cleanup EXIT
42
+ trap 'exit 1' HUP INT TERM
43
+
44
+ note "image $image, bundle from $source_mode"
45
+ docker run -d --name "$name" --pull missing "$image" sleep infinity >/dev/null || fail 'cannot start the container'
46
+ container=$name
47
+ # Clean machine: no Node, npm, pnpm, Pi or Gentle Shell. curl and CA roots are
48
+ # bootstrap prerequisites that a stock Debian desktop already has.
49
+ docker exec "$container" sh -c 'export DEBIAN_FRONTEND=noninteractive
50
+ apt-get update -qq >/dev/null && apt-get install -y -qq --no-install-recommends ca-certificates curl >/dev/null
51
+ useradd --create-home --shell /bin/bash tester
52
+ mkdir /opt/bundle
53
+ for tool in node npm pnpm pi gentle-shell; do if command -v "$tool" >/dev/null; then echo "unexpected $tool" >&2; exit 1; fi; done' \
54
+ || fail 'container preparation failed'
55
+ # gentle-shell setup makes 2 anonymous GitHub API requests (upstream Gentle AI's
56
+ # latest-engram lookup), limited to 60 per hour per public IP. /rate_limit does
57
+ # not consume quota. Stop early instead of failing deep inside setup.
58
+ quota=$(docker exec "$container" sh -c 'curl -fsS --max-time 20 -H "Accept: application/vnd.github+json" https://api.github.com/rate_limit' | tr -d ' \n') \
59
+ || fail 'cannot query the GitHub API rate limit from the container'
60
+ rate=$(printf '%s' "$quota" | sed -n 's/.*"rate":{\([^}]*\)}.*/\1/p')
61
+ remaining=$(printf '%s' "$rate" | sed -n 's/.*"remaining":\([0-9]*\).*/\1/p')
62
+ reset=$(printf '%s' "$rate" | sed -n 's/.*"reset":\([0-9]*\).*/\1/p')
63
+ [ -n "$remaining" ] && [ -n "$reset" ] || fail 'unexpected GitHub rate limit response'
64
+ reset_at=$(date -u -d "@$reset" '+%Y-%m-%d %H:%M:%S UTC' 2>/dev/null || date -u -r "$reset" '+%Y-%m-%d %H:%M:%S UTC' 2>/dev/null || printf 'epoch %s' "$reset")
65
+ if [ "$remaining" -lt 5 ]; then
66
+ fail "only $remaining anonymous GitHub API requests remain on this network (setup needs 2); the quota resets at $reset_at. Not starting."
67
+ fi
68
+ note "GitHub API quota: $remaining anonymous requests left (resets at $reset_at)"
69
+ if [ "$source_mode" = head ]; then
70
+ git -C "$repo" archive --format=tar HEAD
71
+ else
72
+ git -C "$repo" ls-files -z | tar -C "$repo" --null -T - -cf -
73
+ fi | docker exec -i "$container" tar --no-same-owner -xf - -C /opt/bundle || fail 'cannot copy the bundle'
74
+ note "prepared in $(elapsed)"
75
+
76
+ bootstrap_started=$(date +%s)
77
+ # A terminal session exports SHELL (the user's login shell); docker exec does
78
+ # not. pnpm setup needs it to pick the profile to edit, and fails with
79
+ # ERR_PNPM_UNKNOWN_SHELL without it.
80
+ docker exec -d -u tester -w /home/tester -e SHELL=/bin/bash "$container" sh -c \
81
+ 'sh /opt/bundle/scripts/bootstrap.sh > "$HOME/bootstrap.log" 2>&1; echo $? > "$HOME/bootstrap.status"' \
82
+ || fail 'cannot start the bootstrap'
83
+
84
+ # Headless browser stand-in: the same requests the wizard page makes.
85
+ driver=$(cat <<'DRIVER'
86
+ set -eu
87
+ log=$HOME/bootstrap.log
88
+ say() { printf 'driver: %s\n' "$1"; }
89
+ stop() { say "$1"; cat "$log" 2>/dev/null || :; exit 1; }
90
+ waited=0
91
+ while :; do
92
+ url=$(grep -o 'http://127\.0\.0\.1:[0-9]*/session?code=[A-Za-z0-9_-]*' "$log" 2>/dev/null | head -n 1) || url=
93
+ [ -z "$url" ] || break
94
+ [ ! -f "$HOME/bootstrap.status" ] || stop 'bootstrap exited before printing the session URL'
95
+ waited=$((waited + 1)); [ "$waited" -lt 600 ] || stop 'no session URL after 600 s'
96
+ sleep 1
97
+ done
98
+ say "session URL after ${waited}s"
99
+ origin=${url%%/session*}
100
+ jar=$HOME/acceptance-cookies
101
+ curl -fsS -c "$jar" -b "$jar" -o /dev/null "$url" || stop 'session redemption failed'
102
+ api() { curl -fsS -b "$jar" -H 'X-Gentle-Install: 1' "$@"; }
103
+ post() { api -X POST -H "Origin: $origin" -H 'Content-Type: application/json' --data "$1" "$origin$2"; }
104
+ plan=$(api "$origin/api/plan") || stop 'plan request failed'
105
+ case $plan in *'"blockers":[]'*) ;; *) stop "plan has blockers: $plan";; esac
106
+ plan_id=$(printf '%s' "$plan" | sed -n 's/.*"planId":"\([^"]*\)".*/\1/p')
107
+ [ -n "$plan_id" ] || stop "plan has no planId: $plan"
108
+ say "plan: $(printf '%s' "$plan" | grep -o '"actions":\[[^]]*\]' | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | tr '\n' ' ')"
109
+ post "{\"planId\":\"$plan_id\",\"consent\":true}" /api/install >/dev/null || stop 'install request rejected'
110
+ seq=0
111
+ polls=0
112
+ while :; do
113
+ progress=$(api "$origin/api/progress?after=$seq") || stop 'progress request failed'
114
+ printf '%s' "$progress" | grep -o '"step":"[^"]*","status":"[^"]*"' | sed 's/"step":"\([^"]*\)","status":"\([^"]*\)"/ step \1: \2/' || :
115
+ last=$(printf '%s' "$progress" | grep -o '"seq":[0-9]*' | tail -n 1 | cut -d: -f2) || last=
116
+ [ -z "$last" ] || seq=$last
117
+ case $progress in *'"running":false'*'"outcome":{'*) break;; esac
118
+ polls=$((polls + 1)); [ "$polls" -lt "$INSTALL_LIMIT" ] || stop "installation still running after $INSTALL_LIMIT s"
119
+ sleep 1
120
+ done
121
+ outcome=$(printf '%s' "$progress" | sed -n 's/.*"outcome":{"outcome":"\([a-z-]*\)".*/\1/p')
122
+ say "outcome: $outcome"
123
+ post '{}' /api/shutdown >/dev/null || stop 'shutdown request rejected'
124
+ waited=0
125
+ until [ -f "$HOME/bootstrap.status" ]; do
126
+ waited=$((waited + 1)); [ "$waited" -lt 60 ] || stop 'bootstrap still running 60 s after shutdown'
127
+ sleep 1
128
+ done
129
+ status=$(cat "$HOME/bootstrap.status")
130
+ say "bootstrap exit status: $status"
131
+ cat "$log"
132
+ case $outcome in ready|terminal-action-required) [ "$status" = 0 ] || exit 1;; *) exit 1;; esac
133
+ DRIVER
134
+ )
135
+ printf '%s\n' "$driver" | docker exec -i -u tester -w /home/tester -e INSTALL_LIMIT="$install_limit" "$container" sh -s \
136
+ || fail "wizard flow failed after $(elapsed)"
137
+ note "bootstrap and wizard finished in $(( $(date +%s) - bootstrap_started ))s"
138
+
139
+ # A fresh interactive shell must find the persisted stack; the bootstrap tools
140
+ # must be gone. bash -i without a terminal warns about job control; harmless.
141
+ checks=$(cat <<'CHECKS'
142
+ set -eu
143
+ for tool in node npm pnpm gentle-shell; do printf '%s -> %s\n' "$tool" "$(command -v "$tool")"; done
144
+ printf 'node %s, npm %s, pnpm %s\n' "$(node --version)" "$(npm --version)" "$(pnpm --version)"
145
+ gentle-shell --version
146
+ for dir in "$HOME"/.gentle-shell-bootstrap-tools.*; do
147
+ if [ -e "$dir" ] || [ -L "$dir" ]; then echo "bootstrap tools left behind: $dir" >&2; exit 1; fi
148
+ done
149
+ echo 'bootstrap tools removed'
150
+ CHECKS
151
+ )
152
+ docker exec -u tester -w /home/tester "$container" bash -ic "$checks" || fail "fresh-terminal checks failed after $(elapsed)"
153
+ # Informational: pnpm setup edits only ~/.bashrc, which non-interactive login
154
+ # shells skip on Debian (see docs/install-wizard.md).
155
+ if docker exec -u tester -w /home/tester "$container" bash -lc 'command -v gentle-shell' >/dev/null 2>&1; then
156
+ note 'bash -lc resolves gentle-shell'
157
+ else
158
+ note 'bash -lc does not resolve gentle-shell (expected: ~/.profile is not edited)'
159
+ fi
160
+ passed=1
161
+ note "PASS in $(elapsed)"