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
@@ -17,9 +17,9 @@ const ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
17
17
  const { createGentleAiExtension } = await import(pathToFileURL(join(ROOT, "extensions/gentle-ai.ts")).href);
18
18
  const EXTENSIONS = [
19
19
  "extensions/gentle-ai.ts",
20
+ "extensions/nan-provider.ts",
20
21
  "extensions/quiet-tools.ts",
21
22
  "extensions/skill-registry.ts",
22
- "extensions/sdd-init.ts",
23
23
  "extensions/startup-banner.ts",
24
24
  ];
25
25
 
@@ -33,30 +33,16 @@ const EXPECTED_BANNER_COMMANDS = [
33
33
  const EXPECTED_COMMANDS = [
34
34
  "gentle:install-delegation",
35
35
  "gentle:install-review",
36
- "gentle:install-sdd",
37
- "gentle:sdd-preflight",
38
- "gentle-sdd-status",
39
- "gentle-sdd-continue",
40
36
  "gentle:models",
41
37
  "gentle:persona",
42
38
  "gentle:status",
43
39
  "gentle:doctor",
44
- "gentle-sdd-init",
45
40
  "skill-registry:refresh",
46
41
  ...EXPECTED_BANNER_COMMANDS,
47
42
  ];
48
43
 
49
44
  const FORBIDDEN_COMPAT_COMMANDS = [
50
45
  "gentle:install-assets",
51
- // The SDD entry points carry the gentle- prefix so they read identically in
52
- // Claude Code and Pi. The bare names are retired without an alias.
53
- "sdd-init",
54
- "sdd-continue",
55
- "sdd-status",
56
- "gentle-ai:install-sdd",
57
- "gentle-ai:sdd-preflight",
58
- "gentle-ai:sdd-status",
59
- "gentle-ai:sdd-continue",
60
46
  "gentle-ai:models",
61
47
  "gentleman:models",
62
48
  "gentle-ai:persona",
@@ -72,6 +58,7 @@ const FORBIDDEN_COMPAT_COMMANDS = [
72
58
  function createPi() {
73
59
  const hooks = new Map();
74
60
  const commands = new Map();
61
+ const providers = new Map();
75
62
  const flags = new Map();
76
63
  const tools = new Map();
77
64
  const eventHandlers = new Map();
@@ -104,6 +91,10 @@ function createPi() {
104
91
  registerCommand(name, definition) {
105
92
  commands.set(name, definition);
106
93
  },
94
+ registerProvider(name, config) {
95
+ if (typeof name === "object") providers.set(name.id, name);
96
+ else providers.set(name, config);
97
+ },
107
98
  registerFlag(name, definition) {
108
99
  flags.set(name, definition);
109
100
  },
@@ -136,7 +127,7 @@ function createPi() {
136
127
  },
137
128
  };
138
129
 
139
- return { pi, hooks, commands, flags, tools, emittedEvents };
130
+ return { pi, hooks, commands, providers, flags, tools, emittedEvents };
140
131
  }
141
132
 
142
133
  function createUi() {
@@ -217,25 +208,56 @@ function restoreWorkspaceWritePermissions(cwd) {
217
208
  }
218
209
  }
219
210
 
220
- async function loadExtensions(pi) {
211
+ const ownedFixtureRoots = [];
212
+ const previousFixtureEnvironment = new Map([
213
+ "HOME", "USERPROFILE", "XDG_CONFIG_HOME", "PI_CODING_AGENT_DIR",
214
+ "GENTLE_PI_CONFIG_HOME", "GENTLE_PI_AGENT_HOME", "GENTLE_PI_TEST_ASSETS_DIR",
215
+ "GENTLE_PI_AGENTS_PARENT_PERMISSION_FD",
216
+ ].map((key) => [key, process.env[key]]));
217
+
218
+ async function loadExtensions(pi, fixtureDependencies) {
221
219
  for (const [index, rel] of EXTENSIONS.entries()) {
222
220
  const mod = await import(`${pathToFileURL(join(ROOT, rel)).href}?runtime-harness=${index}`);
223
221
  assert.equal(typeof mod.default, "function", `${rel} must export a default function`);
224
- mod.default(pi);
222
+ if (rel === "extensions/gentle-ai.ts") createGentleAiExtension(fixtureDependencies)(pi);
223
+ else mod.default(pi);
225
224
  }
226
225
  }
227
226
 
228
227
  async function run() {
229
228
  const globalConfigHome = await tempWorkspace();
229
+ ownedFixtureRoots.push(globalConfigHome);
230
230
  const globalAgentHome = await tempWorkspace();
231
+ ownedFixtureRoots.push(globalAgentHome);
231
232
  const ambientTestAssetsDir = await tempWorkspace();
233
+ ownedFixtureRoots.push(ambientTestAssetsDir);
232
234
  process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
233
235
  process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
234
236
  process.env.GENTLE_PI_TEST_ASSETS_DIR = ambientTestAssetsDir;
237
+ process.env.HOME = globalAgentHome;
238
+ process.env.USERPROFILE = globalAgentHome;
239
+ process.env.XDG_CONFIG_HOME = join(globalAgentHome, "xdg");
240
+ process.env.PI_CODING_AGENT_DIR = join(globalAgentHome, "pi");
241
+ // Discovery still exercises the default exports under the actual worker
242
+ // role, but must never acquire the worker's parent-permission channel.
243
+ delete process.env.GENTLE_PI_AGENTS_PARENT_PERMISSION_FD;
244
+ const fixtureDependencies = {
245
+ nativeReviewCli: {},
246
+ processEnv: {
247
+ HOME: globalAgentHome, USERPROFILE: globalAgentHome,
248
+ GENTLE_PI_CONFIG_HOME: globalConfigHome, GENTLE_PI_AGENT_HOME: globalAgentHome,
249
+ PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR,
250
+ XDG_CONFIG_HOME: process.env.XDG_CONFIG_HOME,
251
+ GENTLE_PI_AGENTS_CHILD: "0", GENTLE_AI_TELEMETRY: "0",
252
+ },
253
+ resolveTelemetryTriggerBinary: () => join(globalAgentHome, "never-executed"),
254
+ telemetryTriggerSpawn: () => assert.fail("Runtime fixtures must not spawn telemetry"),
255
+ };
235
256
  const globalModelsPath = join(globalConfigHome, "models.json");
236
257
  const globalSubagentsPath = join(globalAgentHome, "subagents.json");
237
- const { pi, hooks, commands, flags, tools, emittedEvents } = createPi();
238
- await loadExtensions(pi);
258
+ const { pi, hooks, commands, providers, flags, tools, emittedEvents } = createPi();
259
+ await loadExtensions(pi, fixtureDependencies);
260
+ assert.equal(providers.get("nan")?.getModels()[0]?.api, "openai-completions", "runtime extension loading registers the NaN provider");
239
261
 
240
262
  // gentle-pi#404: a collect binding that returns the native last-event
241
263
  // closure must terminate after one capture. It must not re-enter a public
@@ -313,7 +335,7 @@ async function run() {
313
335
  },
314
336
  };
315
337
  const lastEventPi = createPi();
316
- createGentleAiExtension({ nativeReviewCli })(lastEventPi.pi);
338
+ createGentleAiExtension({ ...fixtureDependencies, nativeReviewCli })(lastEventPi.pi);
317
339
  const controller = lastEventPi.tools.get("gentle_review");
318
340
  const capture = lastEventPi.tools.get("gentle_review_capture");
319
341
  assert.ok(controller, "runtime must register the public status controller");
@@ -356,15 +378,49 @@ async function run() {
356
378
  for (const name of FORBIDDEN_COMPAT_COMMANDS) {
357
379
  assert.equal(commands.has(name), false, `compat command should not be registered: ${name}`);
358
380
  }
381
+ // Retired SDD command entry points must not register, regardless of unrelated prompt text.
382
+ for (const name of [
383
+ "gentle:install-sdd", "gentle:sdd-preflight", "gentle-sdd-status",
384
+ "gentle-sdd-continue", "gentle-sdd-init", "sdd-init", "sdd-continue",
385
+ "sdd-status", "gentle-ai:install-sdd", "gentle-ai:sdd-preflight",
386
+ "gentle-ai:sdd-status", "gentle-ai:sdd-continue",
387
+ ]) {
388
+ assert.equal(commands.has(name), false, `retired SDD command registered: ${name}`);
389
+ }
359
390
  assert.ok(flags.has("no-skill-registry"), "missing no-skill-registry flag");
360
391
  assert.ok(hooks.has("session_start"), "missing session_start hook");
361
392
  assert.ok(hooks.has("session_shutdown"), "missing session_shutdown hook");
362
- assert.ok(hooks.has("input"), "missing input hook");
393
+ assert.ok(hooks.has("input"), "missing live input provenance hook");
394
+ for (const text of [
395
+ "/sdd-init", "/sdd-continue", "/sdd-status", "/gentle-sdd-init",
396
+ "/gentle-sdd-continue", "/gentle-sdd-status", "/gentle:install-sdd",
397
+ "/gentle:sdd-preflight", "/gentle-ai:install-sdd", "/gentle-ai:sdd-preflight",
398
+ "/gentle-ai:sdd-status", "/gentle-ai:sdd-continue",
399
+ "Inspect this project without changing files.", "/ordinary-unregistered-command",
400
+ ]) {
401
+ const event = { text, source: "interactive", images: [] };
402
+ const before = structuredClone(event);
403
+ const emittedBefore = emittedEvents.length;
404
+ for (const handler of hooks.get("input")) {
405
+ assert.deepEqual(await handler(event, createCtx(ROOT)), { action: "continue" },
406
+ `input must pass through without slash interception: ${text}`);
407
+ }
408
+ assert.deepEqual(event, before, "input text and attachments must remain unchanged");
409
+ assert.equal(emittedEvents.length, emittedBefore, "input must not dispatch slash logic");
410
+ }
363
411
  assert.ok(hooks.has("before_agent_start"), "missing before_agent_start hook");
364
412
  assert.ok(hooks.has("tool_call"), "missing tool_call hook");
365
- for (const toolName of ["read", "bash", "grep", "find", "ls", "edit", "write"]) {
413
+ for (const toolName of ["read", "grep", "find", "ls", "edit", "write"]) {
366
414
  assert.ok(tools.has(toolName), `missing quiet built-in tool renderer ${toolName}`);
367
415
  }
416
+ const codemode = tools.get("codemode");
417
+ assert.ok(codemode, "quiet-tools must decorate the upstream codemode registration");
418
+ assert.equal(codemode.defaultActive, false, "rendering must not activate codemode");
419
+ assert.equal(codemode.renderShell, "self");
420
+ assert.equal(typeof codemode.renderCall, "function");
421
+ assert.equal(typeof codemode.renderResult, "function");
422
+ assert.equal(typeof codemode.prepareLoadout, "function", "upstream loadout policy must survive decoration");
423
+ assert.deepEqual(pi.getActiveTools(), ["read", "bash", "edit", "write"], "card registration must not change tool activation");
368
424
  assert.ok(tools.has("gentle_review"), "missing registered bounded review controller tool");
369
425
  assert.ok(tools.has("gentle_review_scope"), "missing registered bounded review scope tool");
370
426
  assert.deepEqual(
@@ -389,73 +445,90 @@ async function run() {
389
445
  [],
390
446
  "declared extension directory must load without invalid helper modules",
391
447
  );
448
+ assert.ok(
449
+ discovered.extensions.some((extension) => extension.resolvedPath.endsWith(join("extensions", "nan-provider.ts"))),
450
+ "declared extension directory must discover the NaN provider",
451
+ );
452
+
453
+ const nativeNan = discovered.runtime.pendingNativeProviderRegistrations
454
+ .find((entry) => entry.provider.id === "nan")?.provider;
455
+ assert.ok(nativeNan, "actual Pi loader must queue native NaN registration");
456
+ assert.ok(!discovered.runtime.pendingProviderRegistrations.some((entry) => entry.name === "nan"),
457
+ "NaN must not fall back to the legacy empty-key login route");
458
+ await assert.rejects(nativeNan.auth.apiKey.login({
459
+ signal: new AbortController().signal, prompt: async () => "", notify() {},
460
+ }), /non-empty/);
461
+ assert.deepEqual(await nativeNan.auth.apiKey.login({
462
+ signal: new AbortController().signal, prompt: async () => " synthetic-loader-key ", notify() {},
463
+ }), { type: "api_key", key: "synthetic-loader-key" });
392
464
 
393
465
  // orchestrator-lazy-diet: Pi Subagent Model Routing detail (the "do not
394
466
  // pass the `model` parameter by default" / SDD-model-assignment-scoping
395
467
  // rules) moved verbatim to assets/orchestrator-delegation.md; the
396
468
  // always-on combined prompt now only carries a pointer to it. Union read
397
469
  // so these assertions are repointed, not weakened.
398
- const delegationDetail = await readFile(join(ROOT, "assets", "orchestrator-delegation.md"), "utf8");
470
+ const { DELEGATION_MODULES } = await import("./support/orchestrator-modules.ts");
471
+ const delegationDetail = (await Promise.all(DELEGATION_MODULES.map((file) => readFile(join(ROOT, "assets", file), "utf8")))).join("\n\n");
399
472
 
400
473
  const promptCwd = await tempWorkspace();
401
474
  try {
402
475
  const promptHook = hooks.get("before_agent_start")[0];
403
- const promptResult = await promptHook({ systemPrompt: "base" }, createCtx(promptCwd));
404
- assert.match(promptResult.systemPrompt, /base/);
405
- assert.match(promptResult.systemPrompt, /el Gentleman/);
406
- assert.match(promptResult.systemPrompt + delegationDetail, /do not pass the `model` parameter by default/);
407
- assert.match(
408
- promptResult.systemPrompt + delegationDetail,
409
- /SDD model assignment tables apply only to SDD\/Judgment-Day phase agents/,
410
- );
411
- assert.doesNotMatch(promptResult.systemPrompt, /Every Agent tool call MUST include `model`/);
412
- assert.doesNotMatch(promptResult.systemPrompt, /default\s*\|\s*sonnet\s*\|\s*Non-SDD general delegation/);
413
- assert.match(promptResult.systemPrompt, /openspec\/config\.yaml.*not session preflight/s);
414
- assert.match(promptResult.systemPrompt, /Do not mark SDD preflight complete/);
476
+ // gentle-shell#1485: pi-claude-bridge drops a handler-returned systemPrompt
477
+ // and forwards only structured systemPromptOptions, so the harness must
478
+ // land in appendSystemPrompt and the hook must never return a replacement.
479
+ const promptEvent = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
480
+ const promptResult = await promptHook(promptEvent, createCtx(promptCwd));
481
+ assert.equal(promptResult, undefined, "before_agent_start must not return a replacement systemPrompt");
482
+ assert.equal(promptEvent.systemPrompt, "base", "the original systemPrompt field must be left untouched");
483
+ const promptAppended = promptEvent.systemPromptOptions.appendSystemPrompt;
484
+ assert.match(promptAppended, /el Gentleman/);
485
+ assert.match(promptAppended, /Organic Driven Development/);
486
+ assert.doesNotMatch(promptAppended, /## SDD Research Capabilities/);
487
+ assert.match(promptAppended, /review execution contract/);
488
+ assert.doesNotMatch(await readFile(join(ROOT, "extensions", "gentle-ai.ts"), "utf8"), /readCommandSddStatus/);
489
+ assert.match(promptAppended + delegationDetail, /do not pass the `model` parameter by default/);
490
+ assert.doesNotMatch(promptAppended, /Every Agent tool call MUST include `model`/);
415
491
  assert.ok(
416
- promptResult.systemPrompt.includes(
492
+ promptAppended.includes(
417
493
  `Package assets root: \`${join(ROOT, "assets")}\`. Lazy asset paths below are relative to this root.`,
418
494
  ),
419
495
  "parent prompt must declare the one absolute root for relative lazy asset paths",
420
496
  );
421
- assert.match(promptResult.systemPrompt, /`sdd-orchestrator-workflow\.md`/);
422
497
  assert.doesNotMatch(
423
- promptResult.systemPrompt,
498
+ promptAppended,
424
499
  new RegExp(ambientTestAssetsDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
425
500
  "normal runtime must ignore ambient GENTLE_PI_TEST_ASSETS_DIR",
426
501
  );
427
- assert.doesNotMatch(promptResult.systemPrompt, /\{\{GENTLE_PI_SDD_WORKFLOW_PATH\}\}/);
428
502
  delete process.env.GENTLE_PI_TEST_ASSETS_DIR;
429
503
  await rm(ambientTestAssetsDir, { recursive: true, force: true });
430
504
  await writeFile(
431
505
  join(globalConfigHome, "persona.json"),
432
506
  '{"mode":"neutral"}\n',
433
507
  );
434
- const neutralPromptResult = await promptHook({ systemPrompt: "base" }, createCtx(promptCwd));
435
- assert.match(neutralPromptResult.systemPrompt, /Do not use slang or regional expressions/);
508
+ const neutralPromptEvent = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
509
+ const neutralPromptResult = await promptHook(neutralPromptEvent, createCtx(promptCwd));
510
+ assert.equal(neutralPromptResult, undefined, "before_agent_start must not return a replacement systemPrompt");
511
+ const neutralAppended = neutralPromptEvent.systemPromptOptions.appendSystemPrompt;
512
+ assert.match(neutralAppended, /Do not use slang or regional expressions/);
436
513
  assert.doesNotMatch(
437
- neutralPromptResult.systemPrompt,
514
+ neutralAppended,
438
515
  /When the user writes Spanish, answer in natural Rioplatense Spanish with voseo/,
439
516
  "neutral persona prompt must not include unconditional voseo instructions after reload",
440
517
  );
441
- const subagentPromptResult = await promptHook(
442
- { agentName: "worker", systemPrompt: "worker base" },
443
- createCtx(promptCwd),
444
- );
445
- assert.equal(subagentPromptResult.systemPrompt, "worker base");
446
- assert.equal(
447
- existsSync(join(promptCwd, ".pi", "agents", "sdd-apply.md")),
448
- false,
449
- "normal agent startup must not run SDD preflight",
450
- );
518
+ const subagentEvent = { agentName: "worker", systemPrompt: "worker base", systemPromptOptions: { appendSystemPrompt: "" } };
519
+ const subagentPromptResult = await promptHook(subagentEvent, createCtx(promptCwd));
520
+ assert.equal(subagentPromptResult, undefined, "before_agent_start must not return a replacement systemPrompt");
521
+ assert.equal(subagentEvent.systemPromptOptions.appendSystemPrompt, "", "a named agent gets nothing appended");
451
522
  await mkdir(join(promptCwd, ".pi", "gentle-ai"), { recursive: true });
452
523
  await writeFile(
453
524
  join(promptCwd, ".pi", "gentle-ai", "persona.json"),
454
525
  '{"mode":"gentleman"}\n',
455
526
  );
456
- const localOverridePromptResult = await promptHook({ systemPrompt: "base" }, createCtx(promptCwd));
527
+ const localOverrideEvent = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
528
+ const localOverridePromptResult = await promptHook(localOverrideEvent, createCtx(promptCwd));
529
+ assert.equal(localOverridePromptResult, undefined, "before_agent_start must not return a replacement systemPrompt");
457
530
  assert.match(
458
- localOverridePromptResult.systemPrompt,
531
+ localOverrideEvent.systemPromptOptions.appendSystemPrompt,
459
532
  /When the user writes Spanish, answer in natural Rioplatense Spanish with voseo/,
460
533
  );
461
534
  const personaCtx = createCtx(promptCwd, true);
@@ -470,54 +543,6 @@ async function run() {
470
543
  '{\n "mode": "neutral"\n}\n',
471
544
  );
472
545
  assert.match(personaCtx.ui.notifications.at(-1).message, /Global config:/);
473
- const onboardCtx = createCtx(promptCwd, true, "sdd-onboard-session");
474
- onboardCtx.ui.select = async (_label, options) => options[0];
475
- const onboardPromptResult = await promptHook(
476
- { agentName: "sdd-onboard", systemPrompt: "onboard base" },
477
- onboardCtx,
478
- );
479
- assert.match(onboardPromptResult.systemPrompt, /onboard base/);
480
- assert.match(onboardPromptResult.systemPrompt, /## SDD Session Preflight/);
481
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-onboard.md")), true);
482
- await mkdir(join(promptCwd, "openspec", "changes", "status-demo", "specs", "demo"), { recursive: true });
483
- await writeFile(join(promptCwd, "openspec", "changes", "status-demo", "proposal.md"), "# Proposal\n");
484
- await writeFile(join(promptCwd, "openspec", "changes", "status-demo", "specs", "demo", "spec.md"), "# Spec\n");
485
- await writeFile(join(promptCwd, "openspec", "changes", "status-demo", "design.md"), "# Design\n");
486
- await writeFile(join(promptCwd, "openspec", "changes", "status-demo", "tasks.md"), "# Tasks\n\n- [ ] 1.1 Implement demo\n");
487
- const applyPromptResult = await promptHook(
488
- { agentName: "sdd-apply", systemPrompt: "apply base" },
489
- createCtx(promptCwd, true, "sdd-apply-session"),
490
- );
491
- assert.match(applyPromptResult.systemPrompt, /## Native SDD Status Engine/);
492
- assert.match(applyPromptResult.systemPrompt, /"changeName": "status-demo"/);
493
- assert.match(applyPromptResult.systemPrompt, /### apply instructions/);
494
- const statusCtx = createCtx(promptCwd, true);
495
- await commands.get("gentle-sdd-status").handler("status-demo --json", statusCtx);
496
- assert.match(statusCtx.ui.notifications.at(-1).message, /"schemaName": "gentle-ai\.sdd-status"/);
497
- const continueCtx = createCtx(promptCwd, true);
498
- let markerConfirmations = 0;
499
- continueCtx.ui.confirm = async (_title, message) => {
500
- assert.ok(message.includes(join(promptCwd, "openspec/changes/status-demo/.gentle-ai-instance")));
501
- assert.match(message, /no source roots and no persistent authority/);
502
- markerConfirmations += 1;
503
- return true;
504
- };
505
- await commands.get("gentle-sdd-continue").handler("status-demo", continueCtx);
506
- assert.equal(markerConfirmations, 1);
507
- assert.match(continueCtx.ui.notifications.at(-1).message, /Native SDD Status Engine/);
508
- assert.match(continueCtx.ui.notifications.at(-1).message, /"nextRecommended": "apply"/);
509
- const { execFileSync } = await import("node:child_process");
510
- execFileSync("git", ["init"], { cwd: promptCwd, stdio: "ignore" });
511
- const recoveryRequiredDirectory = join(promptCwd, ".git", "gentle-ai", "reviews", "control", "recovery-required-v1");
512
- await mkdir(recoveryRequiredDirectory, { recursive: true });
513
- await writeFile(
514
- join(recoveryRequiredDirectory, `${domainHashV1("openspec-change-name", "status-demo")}.json`),
515
- '{"schema":"gentle-ai.recovery-required/v1","change_name":"status-demo"}',
516
- );
517
- const blockedContinueCtx = createCtx(promptCwd, true);
518
- await commands.get("gentle-sdd-continue").handler("status-demo", blockedContinueCtx);
519
- assert.doesNotMatch(blockedContinueCtx.ui.notifications.at(-1).message, /resolve-review:/);
520
- assert.match(blockedContinueCtx.ui.notifications.at(-1).message, /"nextRecommended": "apply"/);
521
546
  } finally {
522
547
  await rm(promptCwd, { recursive: true, force: true });
523
548
  }
@@ -606,42 +631,6 @@ async function run() {
606
631
  "a writer may dispatch when context carries narrow task-scoped repository-relative paths",
607
632
  );
608
633
 
609
- const interactiveSddDispatch = {
610
- agent: "sdd-remediate",
611
- task: "Correct the bound failed verification evidence.",
612
- context: "Retain the parent-supplied remediation scope.",
613
- mode: "task",
614
- };
615
- assert.equal(
616
- await toolHook({ toolName: "subagent_run", input: interactiveSddDispatch }, createCtx(toolCwd, true, "interactive-sdd-parent")),
617
- undefined,
618
- "an interactive parent may dispatch an SDD child only after preflight resolves",
619
- );
620
- assert.match(
621
- interactiveSddDispatch.context,
622
- /^## SDD Session Preflight\nThese SDD preferences are explicit current-session choices\./,
623
- "the parent-confirmed rendered preflight must be transported through the RPC child's context",
624
- );
625
- const rpcChildCwd = await tempWorkspace();
626
- try {
627
- const rpcChild = createPi();
628
- createGentleAiExtension({ processEnv: { GENTLE_PI_AGENTS_CHILD: "1" } })(rpcChild.pi);
629
- const rpcChildCtx = createCtx(rpcChildCwd, false, "delegated-rpc-sdd-child");
630
- rpcChildCtx.mode = "rpc";
631
- const rpcChildPrompt = await rpcChild.hooks.get("before_agent_start")[0](
632
- { agentName: "sdd-remediate", systemPrompt: "You are the SDD remediate executor for Gentle AI." },
633
- rpcChildCtx,
634
- );
635
- assert.doesNotMatch(rpcChildPrompt.systemPrompt, /## SDD Session Preflight/);
636
- assert.equal(
637
- existsSync(join(rpcChildCwd, ".pi", "gentle-ai", "sdd-preflight.json")),
638
- false,
639
- "a delegated RPC child must not silently originate or persist defaults",
640
- );
641
- } finally {
642
- await rm(rpcChildCwd, { recursive: true, force: true });
643
- }
644
-
645
634
  const canonicalFrozenFindingRow = '{"id":"JD-A-001","lens":"judgment-day","location":"extensions/gentle-ai.ts:1","severity":"CRITICAL","status_at_freeze":"open","evidence_class":"deterministic","evidence_claim":"The frozen finding has concrete user impact."}';
646
635
  const canonicalFrozenFindingRows = [JSON.parse(canonicalFrozenFindingRow)];
647
636
  const canonicalFrozenLedgerHash = canonicalHash(canonicalFrozenFindingRows);
@@ -899,7 +888,7 @@ async function run() {
899
888
  registry.bindCurrent({ token: view.token, lineageId: "harness-candidate-drift", selectedLenses: ["review-risk"] });
900
889
 
901
890
  const dispatchPi = createPi();
902
- createGentleAiExtension({ candidateViews: registry })(dispatchPi.pi);
891
+ createGentleAiExtension({ ...fixtureDependencies, candidateViews: registry })(dispatchPi.pi);
903
892
  const dispatchToolHook = dispatchPi.hooks.get("tool_call")[0];
904
893
 
905
894
  // The contributor edits the tracked file strictly after the candidate
@@ -1030,24 +1019,9 @@ async function run() {
1030
1019
  for (const handler of hooks.get("session_start")) {
1031
1020
  await handler({ reason: "startup" }, createCtx(noUiCwd, false));
1032
1021
  }
1033
- assert.equal(
1034
- existsSync(join(noUiCwd, ".pi", "agents", "sdd-apply.md")),
1035
- false,
1036
- "session_start must not install project-local SDD agents",
1037
- );
1038
- assert.equal(
1039
- existsSync(join(noUiCwd, ".pi", "chains", "sdd-full.chain.md")),
1040
- false,
1041
- "session_start must not install project-local SDD chains",
1042
- );
1043
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), false);
1044
- assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), false);
1045
- assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support")), false);
1046
1022
  for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
1047
1023
  const ctx = createCtx(noUiCwd, true);
1048
1024
  await commands.get(diagnostic).handler("", ctx);
1049
- assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets: on demand/);
1050
- assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /install-sdd --force/);
1051
1025
  }
1052
1026
  // gentle-pi#311 P5: the Pi-authored adversarial role agents are retired;
1053
1027
  // installation must not (re)create them.
@@ -1062,8 +1036,8 @@ async function run() {
1062
1036
  assert.equal(existsSync(installedExplorePath), true);
1063
1037
  assert.deepEqual(
1064
1038
  readAgentDefinition(await readFile(installedExplorePath, "utf8")),
1065
- { name: "gentle-ai-explore", tools: ["read", "grep", "find", "codegraph"] },
1066
- "isolated package installation must activate only the explorer inspection tools",
1039
+ { name: "gentle-ai-explore", tools: ["read", "grep", "find", "codegraph", "web_enable", "web_search", "source_check", "fetch_content", "get_search_content"] },
1040
+ "isolated package installation must declare the explorer's local and direct web scope",
1067
1041
  );
1068
1042
  const installedRiskSource = await readFile(
1069
1043
  join(globalAgentHome, "agents", "review-risk.md"),
@@ -1071,19 +1045,10 @@ async function run() {
1071
1045
  );
1072
1046
  assert.match(installedRiskSource, /exactly once against the supplied `initial_review_tree`/);
1073
1047
  assert.match(installedRiskSource, /cannot authorize transitions, fixes, receipts, gates, or delivery/);
1074
- await commands.get("gentle:sdd-preflight").handler("", createCtx(noUiCwd, false, "startup-sdd-install"));
1075
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1076
- assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
1077
- assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
1078
1048
  const managedAssetsManifestPath = join(globalAgentHome, "gentle-ai", "managed-assets.json");
1079
1049
  const managedAssetsManifest = JSON.parse(
1080
1050
  await readFile(managedAssetsManifestPath, "utf8"),
1081
1051
  );
1082
- const previousManagedApply = "stale global apply\n";
1083
- const previousManagedChain = "stale global chain\n";
1084
- const previousManagedSupport = "stale global status contract\n";
1085
- await writeFile(join(globalAgentHome, "agents", "sdd-apply.md"), previousManagedApply);
1086
- managedAssetsManifest.assets["agents/sdd-apply.md"] = sha256(previousManagedApply);
1087
1052
  const samePathUserRefuter = [
1088
1053
  "---",
1089
1054
  "name: review-refuter",
@@ -1106,60 +1071,25 @@ async function run() {
1106
1071
  await mkdir(join(globalAgentHome, "subagents"), { recursive: true });
1107
1072
  const userRefuterOverride = join(globalAgentHome, "subagents", "review-refuter.md");
1108
1073
  await writeFile(userRefuterOverride, "user refuter override must stay\n");
1109
- await writeFile(join(globalAgentHome, "chains", "sdd-full.chain.md"), previousManagedChain);
1110
- managedAssetsManifest.assets["chains/sdd-full.chain.md"] = sha256(previousManagedChain);
1111
- await writeFile(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md"), previousManagedSupport);
1112
- managedAssetsManifest.assets["gentle-ai/support/sdd-status-contract.md"] = sha256(previousManagedSupport);
1113
1074
  await writeFile(
1114
1075
  managedAssetsManifestPath,
1115
1076
  JSON.stringify(managedAssetsManifest, null, 2),
1116
1077
  );
1117
- await mkdir(join(noUiCwd, ".pi", "agents"), { recursive: true });
1118
- await writeFile(join(noUiCwd, ".pi", "agents", "sdd-apply.md"), "project override must stay\n");
1119
1078
  const projectRefuterOverride = join(noUiCwd, ".pi", "agents", "review-refuter.md");
1079
+ await mkdir(dirname(projectRefuterOverride), { recursive: true });
1120
1080
  await writeFile(projectRefuterOverride, "project refuter override must stay\n");
1121
1081
  for (const handler of hooks.get("session_start")) {
1122
1082
  await handler({ reason: "startup" }, createCtx(noUiCwd, false));
1123
1083
  }
1124
- for (const [key, stale] of [
1125
- ["agents/sdd-apply.md", previousManagedApply],
1126
- ["chains/sdd-full.chain.md", previousManagedChain],
1127
- ["gentle-ai/support/sdd-status-contract.md", previousManagedSupport],
1128
- ]) {
1129
- assert.equal(await readFile(join(globalAgentHome, key), "utf8"), stale,
1130
- "startup must preserve existing SDD package content without explicit routing");
1131
- }
1132
1084
  assert.equal(existsSync(retiredManagedValidatorPath), false, "review retirement still runs at startup");
1133
1085
  const driftCtx = createCtx(noUiCwd, true);
1134
1086
  await commands.get("gentle:status").handler("", driftCtx);
1135
- assert.match(driftCtx.ui.notifications.at(-1).message, /Global SDD assets stale: 3 file\(s\).*install-sdd --force/);
1136
1087
  assert.doesNotMatch(driftCtx.ui.notifications.at(-1).message, /install-(delegation|review) --force/);
1137
- await commands.get("gentle:sdd-preflight").handler("", createCtx(noUiCwd, false, "startup-sdd-refresh"));
1138
- assert.notEqual(
1139
- await readFile(join(globalAgentHome, "agents", "sdd-apply.md"), "utf8"),
1140
- "stale global apply\n",
1141
- "SDD preflight must refresh stale global SDD agents",
1142
- );
1143
1088
  assert.equal(
1144
1089
  await readFile(installedRefuterPath, "utf8"),
1145
1090
  samePathUserRefuter,
1146
1091
  "session refresh must preserve a same-path user-authored agent byte-for-byte",
1147
1092
  );
1148
- assert.notEqual(
1149
- await readFile(join(globalAgentHome, "chains", "sdd-full.chain.md"), "utf8"),
1150
- "stale global chain\n",
1151
- "SDD preflight must refresh stale global SDD chains",
1152
- );
1153
- assert.notEqual(
1154
- await readFile(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md"), "utf8"),
1155
- "stale global status contract\n",
1156
- "SDD preflight must refresh stale global SDD support files",
1157
- );
1158
- assert.equal(
1159
- await readFile(join(noUiCwd, ".pi", "agents", "sdd-apply.md"), "utf8"),
1160
- "project override must stay\n",
1161
- "session_start must not overwrite project-local SDD overrides",
1162
- );
1163
1093
  assert.equal(
1164
1094
  await readFile(projectRefuterOverride, "utf8"),
1165
1095
  "project refuter override must stay\n",
@@ -1193,423 +1123,10 @@ async function run() {
1193
1123
  await rm(noUiCwd, { recursive: true, force: true });
1194
1124
  }
1195
1125
 
1196
- const lazySddCwd = await tempWorkspace();
1197
- const lazyAgentHome = join(lazySddCwd, "agent-home");
1198
- process.env.GENTLE_PI_AGENT_HOME = lazyAgentHome;
1199
- try {
1200
- // This scenario must not inherit SDD definitions from earlier startup tests.
1201
- const globalAgentHome = lazyAgentHome;
1202
- await writeFile(
1203
- globalModelsPath,
1204
- JSON.stringify({ "sdd-apply": { model: "openai/gpt-5", thinking: "high" } }, null, 2),
1205
- );
1206
- const ctx = createCtx(lazySddCwd, true);
1207
- const inputHook = hooks.get("input")[0];
1208
- assert.deepEqual(
1209
- await inputHook({ text: "hola, solo mirando", source: "interactive" }, ctx),
1210
- { action: "continue" },
1211
- );
1212
- assert.deepEqual(
1213
- await inputHook({ text: "what is SDD?", source: "interactive" }, ctx),
1214
- { action: "continue" },
1215
- );
1216
- assert.deepEqual(
1217
- await inputHook({ text: "what can I do with SDD?", source: "interactive" }, ctx),
1218
- { action: "continue" },
1219
- );
1220
- assert.deepEqual(
1221
- await inputHook({ text: "how do I use SDD?", source: "interactive" }, ctx),
1222
- { action: "continue" },
1223
- );
1224
- assert.deepEqual(
1225
- await inputHook({ text: "Can I use SDD?", source: "interactive" }, ctx),
1226
- { action: "continue" },
1227
- );
1228
- assert.deepEqual(
1229
- await inputHook({ text: "don't use sdd for this", source: "interactive" }, ctx),
1230
- { action: "continue" },
1231
- );
1232
- assert.deepEqual(
1233
- await inputHook({ text: "sin usar SDD por ahora", source: "interactive" }, ctx),
1234
- { action: "continue" },
1235
- );
1236
- assert.deepEqual(
1237
- await inputHook({ text: "let's not use SDD for this", source: "interactive" }, ctx),
1238
- { action: "continue" },
1239
- );
1240
- assert.deepEqual(
1241
- await inputHook({ text: "never use SDD here", source: "interactive" }, ctx),
1242
- { action: "continue" },
1243
- );
1244
- assert.deepEqual(
1245
- await inputHook({ text: "no quiero usar SDD por ahora", source: "interactive" }, ctx),
1246
- { action: "continue" },
1247
- );
1248
- assert.deepEqual(
1249
- await inputHook({ text: "I use SDD sometimes", source: "interactive" }, ctx),
1250
- { action: "continue" },
1251
- );
1252
- assert.deepEqual(
1253
- await inputHook({ text: "I'm using SDD in another repo", source: "interactive" }, ctx),
1254
- { action: "continue" },
1255
- );
1256
- assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
1257
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), false);
1258
-
1259
- assert.deepEqual(
1260
- await inputHook({ text: "vamos con sdd", source: "interactive" }, ctx),
1261
- { action: "continue" },
1262
- );
1263
- assert.deepEqual(
1264
- await inputHook({ text: "please use sdd for this change", source: "interactive" }, ctx),
1265
- { action: "continue" },
1266
- );
1267
- assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
1268
- assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
1269
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), false);
1270
- assert.equal(ctx.ui.selections.length, 0, "natural-language SDD text has no input-hook side effect");
1271
-
1272
- assert.deepEqual(
1273
- await inputHook({ text: "/sdd", source: "interactive" }, ctx),
1274
- { action: "continue" },
1275
- );
1276
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1277
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
1278
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), false);
1279
- assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
1280
- assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
1281
- assert.equal(ctx.ui.selections.length, 1, "first slash SDD trigger confirms session suggestions");
1282
- assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
1283
- assert.deepEqual(
1284
- await inputHook({ text: "/sdd plan", source: "interactive" }, ctx),
1285
- { action: "continue" },
1286
- );
1287
- assert.deepEqual(
1288
- await inputHook({ text: "/sdd:plan", source: "interactive" }, ctx),
1289
- { action: "continue" },
1290
- );
1291
- assert.equal(ctx.ui.selections.length, 1, "slash SDD triggers reuse confirmed session choices");
1292
-
1293
- assert.deepEqual(
1294
- await inputHook({ text: "/sdd-plan this change", source: "interactive" }, ctx),
1295
- { action: "continue" },
1296
- );
1297
- assert.equal(existsSync(join(lazySddCwd, ".pi", "agents", "sdd-apply.md")), false);
1298
- assert.equal(existsSync(join(lazySddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
1299
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1300
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
1301
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), false);
1302
- assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
1303
- assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
1304
- const globalSddApply = await readFile(
1305
- join(globalAgentHome, "agents", "sdd-apply.md"),
1306
- "utf8",
1307
- );
1308
- assert.match(globalSddApply, /model: openai\/gpt-5/);
1309
- assert.match(globalSddApply, /thinking: high/);
1310
- const lazySettingsPath = join(lazySddCwd, ".pi", "settings.json");
1311
- if (existsSync(lazySettingsPath)) {
1312
- const lazySettings = JSON.parse(await readFile(lazySettingsPath, "utf8"));
1313
- assert.equal(
1314
- lazySettings.subagents?.agentOverrides?.["sdd-apply"],
1315
- undefined,
1316
- "global SDD model routing must be materialized in agent frontmatter, not project settings overrides",
1317
- );
1318
- }
1319
- assert.equal(ctx.ui.selections.length, 1, "automatic SDD routing reuses session confirmation");
1320
- assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
1321
- await commands.get("gentle:status").handler("", ctx);
1322
- assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets stale: 0 file\(s\)/);
1323
- assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /install-sdd --force/);
1324
-
1325
- await inputHook({ text: "/sdd-plan another change", source: "interactive" }, ctx);
1326
- assert.equal(ctx.ui.selections.length, 1, "resolved preflight must not prompt again in this session");
1327
- const promptHook = hooks.get("before_agent_start")[0];
1328
- const promptResult = await promptHook({ systemPrompt: "base" }, ctx);
1329
- assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
1330
- assert.match(promptResult.systemPrompt, /Execution mode: auto/);
1331
- const workerPromptResult = await promptHook(
1332
- { agentName: "worker", systemPrompt: "worker base" },
1333
- ctx,
1334
- );
1335
- assert.equal(
1336
- workerPromptResult.systemPrompt,
1337
- "worker base",
1338
- "non-SDD subagents must not receive parent harness or SDD preflight prompts",
1339
- );
1340
- } finally {
1341
- process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
1342
- await rm(lazySddCwd, { recursive: true, force: true });
1343
- await rm(globalModelsPath, { force: true });
1344
- }
1345
-
1346
- for (const [index, text] of ["/sdd", "/sdd plan", "/sdd:plan", "/sdd-plan this change", "/gentle-sdd-continue", "/gentle-sdd-status fix-rose --json", "/gentle-sdd-init"].entries()) {
1347
- const slashSddCwd = await tempWorkspace();
1348
- try {
1349
- const ctx = createCtx(slashSddCwd, true, `slash-sdd-session-${index}`);
1350
- const inputHook = hooks.get("input")[0];
1351
- assert.deepEqual(await inputHook({ text, source: "interactive" }, ctx), {
1352
- action: "continue",
1353
- });
1354
- assert.equal(existsSync(join(slashSddCwd, ".pi", "agents", "sdd-apply.md")), false);
1355
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1356
- assert.equal(ctx.ui.selections.length, 1, `${text} must confirm the fresh interactive session`);
1357
- } finally {
1358
- await rm(slashSddCwd, { recursive: true, force: true });
1359
- }
1360
- }
1361
-
1362
- const commandPreflightCwd = await tempWorkspace();
1363
- try {
1364
- const command = commands.get("gentle:sdd-preflight");
1365
- const interactive = createCtx(commandPreflightCwd, true, "explicit-command-regression");
1366
- await command.handler("", interactive);
1367
- const first = interactive.ui.selections.length;
1368
- await command.handler("", interactive);
1369
- const reused = interactive.ui.selections.length;
1370
- const select = interactive.ui.select;
1371
- interactive.ui.select = async (label, options) => {
1372
- const value = await select(label, options);
1373
- return label === "SDD execution mode" ? "interactive" : value;
1374
- };
1375
- interactive.ui.input = async () => "650";
1376
- await command.handler("--edit", interactive);
1377
- const edited = interactive.ui.selections.length;
1378
- await command.handler("", interactive);
1379
- assert.equal(interactive.ui.selections.length, edited, "edited choices also reuse without prompting");
1380
- const saved = JSON.parse(await readFile(join(commandPreflightCwd, ".pi", "gentle-ai", "sdd-preflight.json"), "utf8"));
1381
- assert.equal(saved.executionMode, "interactive");
1382
- assert.equal(saved.reviewBudgetLines, 650);
1383
- const rpc = createCtx(commandPreflightCwd, true, "explicit-command-rpc-regression");
1384
- rpc.mode = "rpc";
1385
- let rpcInputs = 0;
1386
- rpc.ui.input = async (_label, placeholder) => { rpcInputs += 1; return placeholder; };
1387
- await command.handler("", rpc);
1388
- await command.handler("--edit", rpc);
1389
- const noUi = createCtx(commandPreflightCwd, false, "explicit-command-no-ui-regression");
1390
- let noUiInputs = 0;
1391
- noUi.ui.input = async (_label, placeholder) => { noUiInputs += 1; return placeholder; };
1392
- await command.handler("", noUi);
1393
- await command.handler("--edit", noUi);
1394
- assert.deepEqual({ first, reused, edited, rpc: rpc.ui.selections.length, noUi: noUi.ui.selections.length },
1395
- { first: 1, reused: 1, edited: 3, rpc: 0, noUi: 0 },
1396
- "explicit command confirms once, reuses unless --edit, and never prompts headless callers");
1397
- assert.deepEqual({ rpcInputs, noUiInputs }, { rpcInputs: 0, noUiInputs: 0 });
1398
- } finally {
1399
- await rm(commandPreflightCwd, { recursive: true, force: true });
1400
- }
1401
-
1402
- const commandSddCwd = await tempWorkspace();
1403
- try {
1404
- const ctx = createCtx(commandSddCwd, true, "command-session");
1405
- await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
1406
- assert.equal(existsSync(join(commandSddCwd, ".pi", "agents", "sdd-apply.md")), false);
1407
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1408
- assert.equal(ctx.ui.selections.length, 2, "explicit preflight prompts intentional choice fields");
1409
- assert.equal(ctx.ui.selections.some(({ label }) => label === "SDD artifact store"), false, "one-option artifact store must be elided");
1410
- assert.match(ctx.ui.notifications.at(-1).message, /Preference source: explicit session choice/);
1411
- await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
1412
- assert.equal(ctx.ui.selections.length, 4, "--edit remains an intentional re-prompt");
1413
- } finally {
1414
- await rm(commandSddCwd, { recursive: true, force: true });
1415
- }
1416
-
1417
- const sddAgentGuardCwd = await tempWorkspace();
1418
- try {
1419
- const ctx = createCtx(sddAgentGuardCwd, true, "sdd-agent-guard-session");
1420
- const promptHook = hooks.get("before_agent_start")[0];
1421
- const promptResult = await promptHook(
1422
- {
1423
- systemPrompt: "You are the SDD proposal executor for Gentle AI.",
1424
- },
1425
- ctx,
1426
- );
1427
- assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "agents", "sdd-apply.md")), false);
1428
- assert.equal(existsSync(join(sddAgentGuardCwd, ".pi", "chains", "sdd-full.chain.md")), false);
1429
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1430
- assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
1431
- assert.equal(ctx.ui.selections.length, 1, "fresh interactive SDD-agent startup requires confirmation");
1432
- assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
1433
- assert.doesNotMatch(
1434
- promptResult.systemPrompt,
1435
- /el Gentleman Identity and Harness/,
1436
- "SDD executor startup must not receive the parent orchestrator prompt",
1437
- );
1438
- assert.doesNotMatch(
1439
- promptResult.systemPrompt,
1440
- /Work Routing Ladder/,
1441
- "SDD executor startup must not receive parent routing instructions",
1442
- );
1443
- assert.match(ctx.ui.notifications.at(-1).message, /SDD preflight complete/);
1444
-
1445
- const reusedPromptResult = await promptHook(
1446
- {
1447
- agentName: "sdd-tasks",
1448
- systemPrompt: "You are the SDD tasks executor for Gentle AI.",
1449
- },
1450
- ctx,
1451
- );
1452
- assert.equal(ctx.ui.selections.length, 1, "SDD-agent startup reuses confirmed session preferences");
1453
- assert.doesNotMatch(
1454
- reusedPromptResult.systemPrompt,
1455
- /el Gentleman Identity and Harness/,
1456
- "named SDD executor startup must not receive the parent orchestrator prompt",
1457
- );
1458
- } finally {
1459
- await rm(sddAgentGuardCwd, { recursive: true, force: true });
1460
- }
1461
-
1462
- const unresolvedSddCwd = await tempWorkspace();
1463
- try {
1464
- const ctx = createCtx(unresolvedSddCwd, true, "unresolved-preflight-session");
1465
- ctx.ui.select = async () => undefined;
1466
- assert.deepEqual(await hooks.get("input")[0]({ text: "/sdd", source: "interactive" }, ctx), { action: "handled" });
1467
- const result = await hooks.get("before_agent_start")[0]({ agentName: "sdd-explore", systemPrompt: "SDD executor" }, ctx);
1468
- assert.match(result.systemPrompt, /SDD preflight unresolved/);
1469
- assert.match(result.systemPrompt, /STOP: Do not initialize/);
1470
- assert.doesNotMatch(result.systemPrompt, /## SDD Session Preflight/);
1471
- assert.equal(existsSync(join(unresolvedSddCwd, "openspec", "config.yaml")), false);
1472
- assert.equal(existsSync(join(unresolvedSddCwd, ".pi", "gentle-ai", "sdd-preflight.json")), false);
1473
- } finally {
1474
- await rm(unresolvedSddCwd, { recursive: true, force: true });
1475
- }
1476
-
1477
- const noUiSddAgentCwd = await tempWorkspace();
1478
- try {
1479
- const ctx = createCtx(noUiSddAgentCwd, false, "no-ui-sdd-agent-session");
1480
- const promptHook = hooks.get("before_agent_start")[0];
1481
- const promptResult = await promptHook(
1482
- {
1483
- agentName: "sdd-proposal",
1484
- systemPrompt: "You are the SDD proposal executor for Gentle AI.",
1485
- },
1486
- ctx,
1487
- );
1488
- assert.match(promptResult.systemPrompt, /SDD Session Preflight/);
1489
- assert.match(promptResult.systemPrompt, /canonical defaults or persisted choices/);
1490
- assert.equal(ctx.ui.selections.length, 0);
1491
- assert.equal(existsSync(join(noUiSddAgentCwd, ".pi", "agents", "sdd-apply.md")), false);
1492
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1493
- } finally {
1494
- await rm(noUiSddAgentCwd, { recursive: true, force: true });
1495
- }
1496
-
1497
- const invalidPreflightCwd = await tempWorkspace();
1498
- try {
1499
- await writeFile(globalModelsPath, "{ invalid json");
1500
- const ctx = createCtx(invalidPreflightCwd, true, "invalid-preflight-session");
1501
- await commands.get("gentle:sdd-preflight").handler("", ctx);
1502
- assert.equal(ctx.ui.notifications.at(-1).level, "warning");
1503
- assert.match(ctx.ui.notifications.at(-1).message, /Model routing skipped:/);
1504
- assert.match(ctx.ui.notifications.at(-1).message, /invalid JSON or not an object/);
1505
- } finally {
1506
- await rm(invalidPreflightCwd, { recursive: true, force: true });
1507
- await rm(globalModelsPath, { force: true });
1508
- }
1509
-
1510
- const engramSddCwd = await tempWorkspace();
1511
- try {
1512
- pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
1513
- const ctx = createCtx(engramSddCwd, true, "engram-session");
1514
- await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
1515
- assert.deepEqual(ctx.ui.selections[1].options, ["openspec", "engram", "hybrid"]);
1516
- } finally {
1517
- pi.setActiveTools(["read", "bash", "edit", "write"]);
1518
- await rm(engramSddCwd, { recursive: true, force: true });
1519
- }
1520
-
1521
- // Issue #64: selecting engram as the artifact store must not cause
1522
- // /gentle-sdd-init to create openspec/ or openspec/config.yaml.
1523
- const engramSddInitCwd = await tempWorkspace();
1524
- try {
1525
- pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
1526
- const ctx = createCtx(engramSddInitCwd, true, "engram-sdd-init-session");
1527
- ctx.ui.select = async (label, options) => {
1528
- if (label === "SDD artifact store") return "engram";
1529
- return options[0];
1530
- };
1531
- await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
1532
- await commands.get("gentle-sdd-init").handler("", ctx);
1533
- assert.equal(
1534
- existsSync(join(engramSddInitCwd, "openspec")),
1535
- false,
1536
- "/gentle-sdd-init must not create openspec/ when artifactStore is engram",
1537
- );
1538
- assert.equal(
1539
- existsSync(join(engramSddInitCwd, "openspec", "config.yaml")),
1540
- false,
1541
- "/gentle-sdd-init must not write openspec/config.yaml when artifactStore is engram",
1542
- );
1543
- assert.doesNotMatch(
1544
- ctx.ui.notifications.at(-1).message,
1545
- /Wrote openspec\/config\.yaml/,
1546
- "/gentle-sdd-init must not announce openspec/config.yaml when artifactStore is engram",
1547
- );
1548
- assert.match(
1549
- ctx.ui.notifications.at(-1).message,
1550
- /SDD initialized for engram:/,
1551
- );
1552
- assert.equal(ctx.ui.notifications.at(-1).level, "info");
1553
- } finally {
1554
- pi.setActiveTools(["read", "bash", "edit", "write"]);
1555
- await rm(engramSddInitCwd, { recursive: true, force: true });
1556
- }
1557
-
1558
- // Issue #64 counterpart: the engram skip must stay narrow. Selecting both
1559
- // still has to create the full openspec/ scaffold and write config.yaml,
1560
- // so an over-broad skip is caught here instead of in the field.
1561
- const bothSddInitCwd = await tempWorkspace();
1562
- try {
1563
- pi.setActiveTools(["read", "bash", "edit", "write", "mem_save"]);
1564
- const ctx = createCtx(bothSddInitCwd, true, "both-sdd-init-session");
1565
- ctx.ui.select = async (label, options) => {
1566
- if (label === "SDD artifact store") return "hybrid";
1567
- return options[0];
1568
- };
1569
- await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
1570
- await commands.get("gentle-sdd-init").handler("", ctx);
1571
- assert.equal(
1572
- existsSync(join(bothSddInitCwd, "openspec", "specs")),
1573
- true,
1574
- "/gentle-sdd-init must create openspec/specs when artifactStore is both",
1575
- );
1576
- assert.equal(
1577
- existsSync(join(bothSddInitCwd, "openspec", "changes", "archive")),
1578
- true,
1579
- "/gentle-sdd-init must create openspec/changes/archive when artifactStore is both",
1580
- );
1581
- assert.equal(
1582
- existsSync(join(bothSddInitCwd, "openspec", "config.yaml")),
1583
- true,
1584
- "/gentle-sdd-init must write openspec/config.yaml when artifactStore is both",
1585
- );
1586
- assert.match(
1587
- ctx.ui.notifications.at(-1).message,
1588
- /Wrote openspec\/config\.yaml/,
1589
- "/gentle-sdd-init must announce openspec/config.yaml when artifactStore is both",
1590
- );
1591
- assert.equal(ctx.ui.notifications.at(-1).level, "info");
1592
- } finally {
1593
- pi.setActiveTools(["read", "bash", "edit", "write"]);
1594
- await rm(bothSddInitCwd, { recursive: true, force: true });
1595
- }
1596
-
1597
- const directEngramToolCwd = await tempWorkspace();
1598
- try {
1599
- pi.setActiveTools(["read", "bash", "edit", "write", "engram_mem_save"]);
1600
- const ctx = createCtx(directEngramToolCwd, true, "direct-engram-session");
1601
- await commands.get("gentle:sdd-preflight").handler("", ctx);
1602
- assert.equal(ctx.ui.selections.some(({ label }) => label === "SDD artifact store"), false, "unrecognized Engram capability must elide the artifact selector");
1603
- } finally {
1604
- pi.setActiveTools(["read", "bash", "edit", "write"]);
1605
- await rm(directEngramToolCwd, { recursive: true, force: true });
1606
- }
1607
-
1608
- for (const owner of ["sdd", "delegation", "review"]) {
1126
+ for (const owner of ["delegation", "review"]) {
1609
1127
  const fixture = await tempWorkspace();
1610
1128
  const agentHome = join(fixture, "agent-home");
1611
1129
  const representatives = {
1612
- sdd: "sdd-apply.md",
1613
1130
  delegation: "gentle-ai-worker.md",
1614
1131
  review: "review-risk.md",
1615
1132
  };
@@ -1658,7 +1175,7 @@ async function run() {
1658
1175
  assert.equal(existsSync(join(agentHome, "subagents.json")), false,
1659
1176
  "owner installers must not apply model routing");
1660
1177
  await commands.get("gentle:status").handler("", ctx);
1661
- const label = owner === "sdd" ? "SDD" : owner;
1178
+ const label = owner;
1662
1179
  assert.ok(ctx.ui.notifications.at(-1).message.includes(`Global ${label} user overrides: 1 file(s)`));
1663
1180
  } finally {
1664
1181
  process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
@@ -1671,153 +1188,60 @@ async function run() {
1671
1188
  try {
1672
1189
  process.env.GENTLE_PI_AGENT_HOME = join(repairFixture, "agent-home");
1673
1190
  const ctx = createCtx(repairFixture, true);
1674
- await commands.get("gentle:install-sdd").handler("", ctx);
1675
1191
  for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
1676
1192
  await commands.get(diagnostic).handler("", ctx);
1677
1193
  const message = ctx.ui.notifications.at(-1).message;
1678
- assert.match(message, /\/gentle:install-delegation --force/,
1194
+ assert.match(message, /Global delegation assets stale: [1-9]\d* file\(s\).*\/gentle:install-delegation --force/,
1679
1195
  `${diagnostic} must provide a repair for missing delegation assets`);
1680
- assert.match(message, /\/gentle:install-review --force/,
1196
+ assert.match(message, /Global review assets stale: [1-9]\d* file\(s\).*\/gentle:install-review --force/,
1681
1197
  `${diagnostic} must provide a repair for missing review assets`);
1682
1198
  assert.doesNotMatch(message, /install-sdd --force/);
1683
1199
  }
1200
+ await commands.get("gentle:install-delegation").handler("", ctx);
1201
+ await commands.get("gentle:install-review").handler("", ctx);
1684
1202
  const manifest = JSON.parse(await readFile(join(process.env.GENTLE_PI_AGENT_HOME, "gentle-ai", "managed-assets.json"), "utf8"));
1685
1203
  for (const key of Object.keys(manifest.assets)) {
1686
1204
  await rm(join(process.env.GENTLE_PI_AGENT_HOME, key));
1687
1205
  }
1688
1206
  for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
1689
1207
  await commands.get(diagnostic).handler("", ctx);
1690
- assert.match(ctx.ui.notifications.at(-1).message, /Global SDD assets stale: [1-9]\d* file\(s\).*install-sdd --force/);
1691
- assert.doesNotMatch(ctx.ui.notifications.at(-1).message, /on demand/,
1692
- "managed installation evidence must survive missing SDD files");
1208
+ const message = ctx.ui.notifications.at(-1).message;
1209
+ assert.match(message, /Global delegation assets stale: [1-9]\d* file\(s\).*\/gentle:install-delegation --force/);
1210
+ assert.match(message, /Global review assets stale: [1-9]\d* file\(s\).*\/gentle:install-review --force/);
1211
+ assert.doesNotMatch(message, /install-sdd --force|on demand/,
1212
+ "managed installation evidence must survive missing delegation and review files");
1693
1213
  }
1694
1214
  } finally {
1695
1215
  process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
1696
1216
  await rm(repairFixture, { recursive: true, force: true });
1697
1217
  }
1698
1218
 
1699
- for (const trigger of ["gentle:sdd-preflight", "gentle-sdd-init"]) {
1700
- const fixture = await tempWorkspace();
1701
- const agentHome = join(fixture, "agent-home");
1702
- try {
1703
- process.env.GENTLE_PI_AGENT_HOME = agentHome;
1704
- const command = commands.get(trigger);
1705
- await command.handler("", createCtx(fixture, true, `${trigger}-install`));
1706
- assert.equal(existsSync(join(agentHome, "agents", "gentle-ai-worker.md")), false,
1707
- `${trigger} must not install delegation assets`);
1708
- assert.equal(existsSync(join(agentHome, "agents", "review-risk.md")), false,
1709
- `${trigger} must not install review assets`);
1710
- const manifestPath = join(agentHome, "gentle-ai", "managed-assets.json");
1711
- const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
1712
- const managedKeys = [
1713
- "agents/sdd-apply.md",
1714
- "chains/sdd-full.chain.md",
1715
- "gentle-ai/support/sdd-status-contract.md",
1716
- ];
1717
- const originals = new Map();
1718
- for (const key of managedKeys) {
1719
- originals.set(key, await readFile(join(agentHome, key), "utf8"));
1720
- }
1721
- const untouchedKeys = ["agents/gentle-ai-worker.md", "agents/review-risk.md"];
1722
- for (const key of [...managedKeys, ...untouchedKeys]) {
1723
- await writeFile(join(agentHome, key), `stale ${key}\n`);
1724
- manifest.assets[key] = sha256(`stale ${key}\n`);
1725
- }
1726
- await writeFile(manifestPath, JSON.stringify(manifest, null, 2));
1727
- const userPath = join(agentHome, "agents", "sdd-spec.md");
1728
- const userEdit = `${await readFile(userPath, "utf8")}\nUser-owned instructions.\n`;
1729
- await writeFile(userPath, userEdit);
1730
- const projectOverride = join(fixture, ".pi", "agents", "sdd-apply.md");
1731
- await mkdir(dirname(projectOverride), { recursive: true });
1732
- await writeFile(projectOverride, "Project override must survive.\n");
1733
- await command.handler("", createCtx(fixture, true, `${trigger}-refresh`));
1734
- for (const key of managedKeys) {
1735
- assert.equal(await readFile(join(agentHome, key), "utf8"), originals.get(key),
1736
- `${trigger} must refresh managed ${key} on demand`);
1737
- }
1738
- const refreshed = JSON.parse(await readFile(manifestPath, "utf8"));
1739
- for (const key of untouchedKeys) {
1740
- assert.equal(await readFile(join(agentHome, key), "utf8"), `stale ${key}\n`);
1741
- assert.equal(refreshed.assets[key], manifest.assets[key]);
1742
- }
1743
- assert.equal(await readFile(userPath, "utf8"), userEdit);
1744
- assert.equal(await readFile(projectOverride, "utf8"), "Project override must survive.\n");
1745
- } finally {
1746
- process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
1747
- await rm(fixture, { recursive: true, force: true });
1748
- }
1749
- }
1750
-
1751
- const installCwd = await tempWorkspace();
1752
- try {
1753
- const ctx = createCtx(installCwd, true);
1754
- await commands.get("gentle:install-sdd").handler("", ctx);
1755
- assert.match(ctx.ui.notifications.at(-1).message, /Global Gentle AI SDD assets installed/);
1756
- assert.equal(existsSync(join(installCwd, ".pi", "agents", "sdd-apply.md")), false);
1757
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1758
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
1759
- assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
1760
- } finally {
1761
- await rm(installCwd, { recursive: true, force: true });
1762
- }
1763
-
1764
1219
  const staleAssetsCwd = await tempWorkspace();
1765
1220
  const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
1766
1221
  const previousHome = process.env.HOME;
1767
1222
  const previousUserProfile = process.env.USERPROFILE;
1768
1223
  try {
1769
- const diagnosticsAgentHome = join(staleAssetsCwd, "agent-home");
1770
- const diagnosticsHome = staleAssetsCwd;
1771
- process.env.GENTLE_PI_AGENT_HOME = diagnosticsAgentHome;
1772
- process.env.HOME = diagnosticsHome;
1773
- process.env.USERPROFILE = diagnosticsHome;
1774
- for (const [dir, name] of [
1775
- [join(diagnosticsAgentHome, "subagents"), "sdd-apply.md"],
1776
- [join(diagnosticsHome, ".agents"), "sdd-archive.md"],
1777
- [join(staleAssetsCwd, ".agents"), "sdd-design.md"],
1778
- [join(staleAssetsCwd, ".pi", "agents"), "sdd-spec.md"],
1779
- [join(staleAssetsCwd, ".pi", "subagents"), "sdd-sync.md"],
1780
- ]) {
1781
- await mkdir(dir, { recursive: true });
1782
- await writeFile(
1783
- join(dir, name),
1784
- `---\nname: ${name.replace(/\.md$/, "")}\n---\nintentional SDD override\n`,
1785
- );
1786
- }
1787
- await mkdir(join(diagnosticsAgentHome, "subagents", "nested"), { recursive: true });
1788
- await mkdir(join(diagnosticsAgentHome, "agents", "nested"), { recursive: true });
1789
- await mkdir(join(staleAssetsCwd, ".agents", "skills", "ignored"), { recursive: true });
1790
- await writeFile(
1791
- join(diagnosticsAgentHome, "subagents", "nested", "renamed-agent.md"),
1792
- "---\nname: sdd-apply\n---\nintentional nested SDD override\n",
1793
- );
1794
- await writeFile(
1795
- join(diagnosticsAgentHome, "agents", "nested", "managed-copy.md"),
1796
- "---\nname: sdd-apply\n---\npackage-managed root is excluded\n",
1797
- );
1798
- await writeFile(
1799
- join(staleAssetsCwd, ".agents", "skills", "ignored", "SKILL.md"),
1800
- "---\nname: sdd-apply\n---\nskills are not agent definitions\n",
1801
- );
1802
- await mkdir(join(staleAssetsCwd, ".pi", "chains"), { recursive: true });
1803
- await mkdir(join(staleAssetsCwd, ".pi", "gentle-ai", "support"), { recursive: true });
1804
- await writeFile(join(staleAssetsCwd, ".pi", "chains", "sdd-full.chain.md"), "stale chain\n");
1805
- await writeFile(join(staleAssetsCwd, ".pi", "gentle-ai", "support", "sdd-status-contract.md"), "stale status contract\n");
1224
+ process.env.GENTLE_PI_AGENT_HOME = join(staleAssetsCwd, "agent-home");
1225
+ process.env.HOME = staleAssetsCwd;
1226
+ process.env.USERPROFILE = staleAssetsCwd;
1806
1227
  const ctx = createCtx(staleAssetsCwd, true);
1807
- await commands.get("gentle:status").handler("", ctx);
1808
- assert.match(ctx.ui.notifications.at(-1).message, /Active SDD agent overrides: 5 file\(s\)/);
1809
- assert.match(ctx.ui.notifications.at(-1).message, /active non-builtin SDD agents shadow package assets/);
1810
1228
  await commands.get("gentle:doctor").handler("", ctx);
1811
1229
  assert.match(ctx.ui.notifications.at(-1).message, /el Gentleman doctor/);
1812
1230
  assert.match(ctx.ui.notifications.at(-1).message, /Sensitive-path guard active/);
1231
+ for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
1232
+ await commands.get(diagnostic).handler("", ctx);
1233
+ const message = ctx.ui.notifications.at(-1).message;
1234
+ assert.match(message, /Organic Driven Development \(ODD\): active/);
1235
+ assert.doesNotMatch(message, /OpenSpec|openspec\/config\.yaml|SDD/);
1236
+ }
1813
1237
  pi.setActiveTools([{ name: "engram.mem_save" }]);
1814
1238
  await commands.get("gentle:doctor").handler("", ctx);
1815
1239
  assert.match(ctx.ui.notifications.at(-1).message, /Engram memory tools active/);
1816
1240
  pi.setActiveTools([{ name: "engram_mem_save" }]);
1817
1241
  await commands.get("gentle:doctor").handler("", ctx);
1818
1242
  assert.match(ctx.ui.notifications.at(-1).message, /Engram memory tools not active in this session/);
1819
- pi.setActiveTools(["read", "bash", "edit", "write"]);
1820
1243
  } finally {
1244
+ pi.setActiveTools(["read", "bash", "edit", "write"]);
1821
1245
  if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
1822
1246
  else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
1823
1247
  if (previousHome === undefined) delete process.env.HOME;
@@ -1827,59 +1251,6 @@ async function run() {
1827
1251
  await rm(staleAssetsCwd, { recursive: true, force: true });
1828
1252
  }
1829
1253
 
1830
- const sddCwd = await tempWorkspace();
1831
- try {
1832
- const ctx = createCtx(sddCwd, true);
1833
- await commands.get("gentle-sdd-init").handler("", ctx);
1834
- assert.equal(existsSync(join(sddCwd, ".pi", "agents", "sdd-apply.md")), false);
1835
- assert.equal(existsSync(join(sddCwd, ".pi", "chains", "sdd-full.chain.md")), false);
1836
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-apply.md")), true);
1837
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-status.md")), true);
1838
- assert.equal(existsSync(join(globalAgentHome, "agents", "sdd-sync.md")), false);
1839
- assert.equal(existsSync(join(globalAgentHome, "gentle-ai", "support", "sdd-status-contract.md")), true);
1840
- assert.equal(existsSync(join(globalAgentHome, "chains", "sdd-full.chain.md")), true);
1841
- assert.equal(ctx.ui.selections.length, 1, "sdd-init confirms session preflight before project initialization");
1842
- assert.match(ctx.ui.notifications[1].message, /SDD preflight complete/);
1843
- assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
1844
- const initializedConfig = await readFile(join(sddCwd, "openspec", "config.yaml"), "utf8");
1845
- const explore = await hooks.get("before_agent_start")[0]({ agentName: "sdd-explore", systemPrompt: "You are the SDD explore executor for Gentle AI." }, ctx);
1846
- assert.match(explore.systemPrompt, /explicit current-session choices/);
1847
- assert.equal(ctx.ui.selections.length, 1, "confirmation -> sdd-init -> explore must reuse the resolved preflight");
1848
- const nextSession = createCtx(sddCwd, true, "cold-start-with-saved-preferences");
1849
- await hooks.get("input")[0]({ text: "/sdd", source: "interactive" }, nextSession);
1850
- assert.equal(nextSession.ui.selections.length, 1, "saved preferences still require confirmation in a new session");
1851
- assert.equal(await readFile(join(sddCwd, "openspec", "config.yaml"), "utf8"), initializedConfig, "new session confirmation must not reset project initialization");
1852
-
1853
- await commands.get("gentle:sdd-preflight").handler("--edit", ctx);
1854
- assert.equal(ctx.ui.selections.length, 3, "--edit permits changes after confirmed sdd-init");
1855
- } finally {
1856
- await rm(sddCwd, { recursive: true, force: true });
1857
- }
1858
-
1859
- const invalidSddInitCwd = await tempWorkspace();
1860
- try {
1861
- await mkdir(join(invalidSddInitCwd, ".pi", "agents"), { recursive: true });
1862
- await writeFile(
1863
- join(invalidSddInitCwd, ".pi", "agents", "sdd-apply.md"),
1864
- `---\nname: sdd-apply\ndescription: Apply phase\nmodel: keep/provider-model\n---\n\nbody\n`,
1865
- );
1866
- await writeFile(globalModelsPath, "{ invalid json");
1867
- const ctx = createCtx(invalidSddInitCwd, true, "invalid-sdd-init-session");
1868
- await commands.get("gentle-sdd-init").handler("", ctx);
1869
- assert.equal(ctx.ui.notifications[1].level, "warning");
1870
- assert.match(ctx.ui.notifications[1].message, /Model routing skipped:/);
1871
- assert.match(ctx.ui.notifications[1].message, /models\.json/);
1872
- assert.match(ctx.ui.notifications.at(-1).message, /Wrote openspec\/config\.yaml/);
1873
- const preservedAgent = await readFile(
1874
- join(invalidSddInitCwd, ".pi", "agents", "sdd-apply.md"),
1875
- "utf8",
1876
- );
1877
- assert.match(preservedAgent, /model: keep\/provider-model/);
1878
- } finally {
1879
- await rm(invalidSddInitCwd, { recursive: true, force: true });
1880
- await rm(globalModelsPath, { force: true });
1881
- }
1882
-
1883
1254
  const legacyModelsCwd = await tempWorkspace();
1884
1255
  try {
1885
1256
  await mkdir(join(legacyModelsCwd, ".pi", "agents"), { recursive: true });
@@ -1963,15 +1334,15 @@ async function run() {
1963
1334
  );
1964
1335
  await writeFile(globalModelsPath, JSON.stringify({ "sdd-apply": {} }, null, 2));
1965
1336
  await hooks.get("session_start")[0]({ reason: "startup" }, legacyCtx);
1966
- const explicitInheritClearsAgent = await readFile(
1337
+ const savedInheritPreservesAgent = await readFile(
1967
1338
  join(legacyModelsCwd, ".pi", "agents", "sdd-apply.md"),
1968
1339
  "utf8",
1969
1340
  );
1970
- assert.doesNotMatch(explicitInheritClearsAgent, /model:/);
1971
- const explicitInheritClearsProfiles = JSON.parse(
1341
+ assert.match(savedInheritPreservesAgent, /model: global\/provider-model/);
1342
+ const savedInheritPreservesProfiles = JSON.parse(
1972
1343
  await readFile(join(legacyModelsCwd, ".pi", "subagents.json"), "utf8"),
1973
1344
  );
1974
- assert.equal(explicitInheritClearsProfiles.model_profiles, undefined);
1345
+ assert.equal(savedInheritPreservesProfiles.model_profiles["sdd-apply"].model, "global/provider-model");
1975
1346
  } finally {
1976
1347
  await rm(legacyModelsCwd, { recursive: true, force: true });
1977
1348
  await rm(globalModelsPath, { force: true });
@@ -1993,10 +1364,12 @@ async function run() {
1993
1364
  );
1994
1365
  await writeFile(globalModelsPath, JSON.stringify({ worker: {} }, null, 2));
1995
1366
  await hooks.get("session_start")[0]({ reason: "startup" }, createCtx(staleSettingsOnlyCwd, true));
1996
- const staleOnlyClearedProfiles = JSON.parse(
1367
+ const staleOnlyPreservedProfiles = JSON.parse(
1997
1368
  await readFile(globalSubagentsPath, "utf8"),
1998
1369
  );
1999
- assert.equal(staleOnlyClearedProfiles.model_profiles, undefined);
1370
+ assert.deepEqual(staleOnlyPreservedProfiles.model_profiles, {
1371
+ worker: { model: "stale/model", effort: "high" },
1372
+ });
2000
1373
  } finally {
2001
1374
  await rm(staleSettingsOnlyCwd, { recursive: true, force: true });
2002
1375
  await rm(globalModelsPath, { force: true });
@@ -2212,22 +1585,29 @@ async function run() {
2212
1585
  assert.match(preservedProjectSubagentWorker, /thinking: medium/);
2213
1586
  await writeFile(globalModelsPath, JSON.stringify({ worker: {} }, null, 2));
2214
1587
  await hooks.get("session_start")[0]({ reason: "startup" }, createCtx(modelsCwd, true));
2215
- const clearedProfiles = JSON.parse(
1588
+ // gentle-ai#4946: startup/reload never treats persisted inherit as
1589
+ // deletion approval, including for previously materialized routing.
1590
+ // Explicit panel saves and confirmed profile replacement still clear.
1591
+ const preservedClearProfiles = JSON.parse(
2216
1592
  await readFile(join(modelsCwd, ".pi", "subagents.json"), "utf8"),
2217
1593
  );
2218
- assert.equal(clearedProfiles.model_profiles, undefined);
1594
+ assert.equal(
1595
+ preservedClearProfiles.model_profiles.worker.model,
1596
+ "existing/model",
1597
+ );
1598
+ assert.equal(preservedClearProfiles.model_profiles.worker.effort, "high");
2219
1599
  const unchangedProjectWorker = await readFile(
2220
1600
  join(modelsCwd, ".pi", "agents", "worker.md"),
2221
1601
  "utf8",
2222
1602
  );
2223
1603
  assert.match(unchangedProjectWorker, /model: existing\/project-worker/);
2224
1604
  assert.match(unchangedProjectWorker, /thinking: high/);
2225
- const clearedProjectSubagentWorker = await readFile(
1605
+ const preservedClearProjectSubagentWorker = await readFile(
2226
1606
  join(modelsCwd, ".pi", "subagents", "worker.md"),
2227
1607
  "utf8",
2228
1608
  );
2229
- assert.doesNotMatch(clearedProjectSubagentWorker, /model:/);
2230
- assert.doesNotMatch(clearedProjectSubagentWorker, /thinking:/);
1609
+ assert.match(preservedClearProjectSubagentWorker, /model: existing\/project-subagent-worker/);
1610
+ assert.match(preservedClearProjectSubagentWorker, /thinking: medium/);
2231
1611
 
2232
1612
  await writeFile(
2233
1613
  globalModelsPath,
@@ -2240,7 +1620,7 @@ async function run() {
2240
1620
  { provider: "evil\u001b]52;c;Zm9v\u0007", id: "model" },
2241
1621
  ];
2242
1622
  ctx.ui.custom = (factory) => {
2243
- const panel = factory(null, null, null, () => undefined);
1623
+ const panel = factory({ terminal: { rows: 24 } }, null, null, () => undefined);
2244
1624
  const initialLines = panel.render(120);
2245
1625
  const plainInitialLines = initialLines.map(stripAnsi);
2246
1626
  assert.ok(
@@ -2248,8 +1628,8 @@ async function run() {
2248
1628
  "model panel should render inside a bordered card",
2249
1629
  );
2250
1630
  assert.ok(
2251
- initialLines.length <= 20,
2252
- "long model agent list should fit within a 24-row terminal 85% overlay budget",
1631
+ initialLines.length === 24,
1632
+ "model panel should fill the 24-row terminal like /gentle:profiles",
2253
1633
  );
2254
1634
  assert.ok(
2255
1635
  plainInitialLines.some((line) => /↓ \d+ more agent\(s\)/.test(line)),
@@ -2273,8 +1653,8 @@ async function run() {
2273
1653
  const scrolledLines = panel.render(120);
2274
1654
  const plainScrolledLines = scrolledLines.map(stripAnsi);
2275
1655
  assert.ok(
2276
- scrolledLines.length <= 20,
2277
- "scrolled model agent list should stay within the overlay height budget",
1656
+ scrolledLines.length === 24,
1657
+ "scrolled model agent list should keep filling the terminal",
2278
1658
  );
2279
1659
  assert.ok(
2280
1660
  plainScrolledLines.some((line) => /↑ \d+ more agent\(s\)/.test(line)),
@@ -2284,8 +1664,8 @@ async function run() {
2284
1664
  const bottomLines = panel.render(120);
2285
1665
  const plainBottomLines = bottomLines.map(stripAnsi);
2286
1666
  assert.ok(
2287
- bottomLines.length <= 20,
2288
- "bottom model agent list should stay within the overlay height budget",
1667
+ bottomLines.length === 24,
1668
+ "bottom model agent list should keep filling the terminal",
2289
1669
  );
2290
1670
  assert.ok(
2291
1671
  plainBottomLines.some((line) => line.includes("▸ ← Back")),
@@ -2532,4 +1912,10 @@ async function run() {
2532
1912
  run().catch((error) => {
2533
1913
  console.error(error);
2534
1914
  process.exitCode = 1;
1915
+ }).finally(async () => {
1916
+ for (const [key, value] of previousFixtureEnvironment) {
1917
+ if (value === undefined) delete process.env[key];
1918
+ else process.env[key] = value;
1919
+ }
1920
+ for (const root of ownedFixtureRoots) await rm(root, { recursive: true, force: true });
2535
1921
  });