gentle-pi 3.7.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. package/README.md +37 -6
  2. package/assets/agents/gentle-ai-explore.md +4 -4
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -9
  5. package/assets/orchestrator-delegation.md +40 -41
  6. package/assets/orchestrator-memory.md +1 -22
  7. package/assets/orchestrator-skills.md +1 -1
  8. package/assets/orchestrator.md +12 -24
  9. package/assets/support/strict-tdd-verify.md +4 -266
  10. package/assets/support/strict-tdd.md +8 -360
  11. package/bin/gentle-shell.mjs +254 -29
  12. package/docs/delegated-verification.md +26 -1
  13. package/docs/gentle-agents-activity.md +24 -0
  14. package/docs/gentle-shell.md +92 -28
  15. package/docs/native-authority-architecture.md +2 -2
  16. package/docs/prompt-history.md +280 -0
  17. package/docs/readme-reference.md +112 -206
  18. package/docs/telemetry.md +1 -1
  19. package/docs/yolo-mode.md +86 -0
  20. package/extensions/child-context.ts +26 -0
  21. package/extensions/child-safety.ts +23 -0
  22. package/extensions/gentle-agents.ts +429 -262
  23. package/extensions/gentle-ai.ts +781 -683
  24. package/extensions/gentle-shell.ts +1375 -88
  25. package/extensions/gentle-stats.ts +101 -0
  26. package/extensions/gentle-todo.ts +18 -7
  27. package/extensions/history/atomic-write.ts +38 -0
  28. package/extensions/history/hide-prompts.ts +183 -0
  29. package/extensions/history/index.ts +1419 -0
  30. package/extensions/history/load-shared-history.ts +39 -0
  31. package/extensions/history/selector-helpers.ts +538 -0
  32. package/extensions/history/session-scan.ts +233 -0
  33. package/extensions/history/store.ts +1119 -0
  34. package/extensions/nan-provider.ts +6 -0
  35. package/extensions/quiet-tools.ts +179 -87
  36. package/extensions/resume-hint.ts +60 -0
  37. package/extensions/skill-registry.ts +16 -12
  38. package/extensions/startup-banner.ts +60 -31
  39. package/lib/agent-assets.ts +604 -0
  40. package/lib/agent-profile-pin.ts +12 -0
  41. package/lib/agents-message-delivery.ts +181 -0
  42. package/lib/agents-protocol.ts +60 -0
  43. package/lib/agents-runner.ts +96 -98
  44. package/lib/agents-view.ts +26 -3
  45. package/lib/agents-widget.ts +16 -9
  46. package/lib/append-system-prompt.ts +21 -0
  47. package/lib/bounded-writer-admission.ts +147 -0
  48. package/lib/card-style-policy.ts +60 -0
  49. package/lib/child-context-files.ts +166 -0
  50. package/lib/codemode-renderer.ts +185 -0
  51. package/lib/command-palette-catalog.ts +3 -9
  52. package/lib/command-palette.ts +25 -14
  53. package/lib/destructive-command-guard.ts +144 -0
  54. package/lib/gentle-ai-elapsed-store.ts +87 -0
  55. package/lib/gentle-ai-renderer.ts +196 -39
  56. package/lib/gentle-shell-launcher.ts +24 -13
  57. package/lib/gentle-shell-resume-hint.ts +176 -0
  58. package/lib/history-capture-policy.ts +95 -0
  59. package/lib/model-routing-authority.ts +5 -1
  60. package/lib/nan-provider.ts +227 -0
  61. package/lib/native-review-cli.ts +49 -108
  62. package/lib/odd-phase-inference.ts +231 -0
  63. package/lib/odd-phase.ts +141 -0
  64. package/lib/overlay-repaint.ts +26 -0
  65. package/lib/pi-tui-keys.ts +53 -0
  66. package/lib/review-candidate-view-owner.ts +67 -17
  67. package/lib/review-candidate-view.ts +112 -25
  68. package/lib/review-reminder-receipt.ts +48 -8
  69. package/lib/review-risk-assessment.ts +156 -11
  70. package/lib/review-sidebar-state.ts +223 -0
  71. package/lib/selection-engine.ts +515 -0
  72. package/lib/session-messaging-grants.ts +135 -0
  73. package/lib/session-worktree-registry.ts +14 -2
  74. package/lib/shell-bar.ts +179 -24
  75. package/lib/shell-card.ts +287 -18
  76. package/lib/shell-changes-view.ts +2 -1
  77. package/lib/shell-prompt.ts +102 -6
  78. package/lib/shell-sidebar-layout.ts +78 -14
  79. package/lib/shell-sidebar.ts +15 -1
  80. package/lib/shell-todo.ts +23 -13
  81. package/lib/shell-usage-view.ts +9 -4
  82. package/lib/shell-usage.ts +66 -10
  83. package/lib/stats-collector.ts +381 -0
  84. package/lib/stats-view.ts +431 -0
  85. package/lib/theme-customization.ts +52 -0
  86. package/lib/vim-editor-adapter.ts +379 -0
  87. package/lib/vim-normal-engine.ts +154 -0
  88. package/lib/vim-operator-engine.ts +416 -0
  89. package/lib/vim-policy.ts +49 -0
  90. package/lib/vim-visual-engine.ts +107 -0
  91. package/lib/visual-customization-policy.ts +108 -0
  92. package/lib/visual-customize-view.ts +330 -0
  93. package/lib/visual-profiles.ts +228 -0
  94. package/lib/yolo-session-policy.ts +240 -0
  95. package/package.json +20 -8
  96. package/runtime/gentle-shell-launcher.mjs +23 -12
  97. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  98. package/runtime/native-review-cli.mjs +49 -108
  99. package/runtime/review-risk-assessment.mjs +154 -9
  100. package/scripts/build-runtime-modules.mjs +1 -0
  101. package/scripts/gentle-ai-installer.mjs +14 -13
  102. package/scripts/mirror-odd-routing.mjs +2 -2
  103. package/scripts/run-test-suite.mjs +76 -0
  104. package/scripts/test-packed-runner.mjs +31 -14
  105. package/scripts/verify-package-files.mjs +11 -22
  106. package/skills/branch-pr/SKILL.md +24 -52
  107. package/skills/chained-pr/SKILL.md +31 -15
  108. package/skills/chained-pr/references/chaining-details.md +31 -20
  109. package/skills/gentle-ai/SKILL.md +9 -15
  110. package/skills/issue-creation/SKILL.md +8 -2
  111. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  112. package/skills/work-unit-commits/SKILL.md +4 -3
  113. package/tests/agent-profiles.test.ts +18 -0
  114. package/tests/agents-fake-child.ts +2 -2
  115. package/tests/agents-message-delivery.test.ts +106 -0
  116. package/tests/agents-protocol.test.ts +40 -0
  117. package/tests/agents-runner.test.ts +378 -89
  118. package/tests/agents-view-thread-identity.test.ts +169 -0
  119. package/tests/agents-view.test.ts +8 -2
  120. package/tests/agents-widget.test.ts +154 -15
  121. package/tests/append-system-prompt-route.test.ts +160 -0
  122. package/tests/append-system-prompt.test.ts +46 -0
  123. package/tests/artifact-language.test.ts +19 -213
  124. package/tests/ask-user-question.test.ts +44 -1
  125. package/tests/asset-installation-runtime.test.ts +5 -16
  126. package/tests/autonomous-guard.test.ts +69 -1
  127. package/tests/bounded-writer-admission.test.ts +95 -0
  128. package/tests/branch-pr-skill.test.ts +43 -0
  129. package/tests/card-style-policy.test.ts +55 -0
  130. package/tests/chained-pr-skill.test.ts +124 -0
  131. package/tests/child-context-files.test.ts +255 -0
  132. package/tests/child-safety.test.ts +82 -0
  133. package/tests/codemode-rendering.test.ts +491 -0
  134. package/tests/command-palette.test.ts +39 -3
  135. package/tests/delegated-key-learnings-contract.test.ts +0 -76
  136. package/tests/destructive-command-guard.test.ts +84 -0
  137. package/tests/devbinary/native-review-parity.devtest.ts +170 -2
  138. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  139. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  140. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  141. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  142. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  143. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  144. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  145. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  146. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  147. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  148. package/tests/generic-agent-tools.test.ts +54 -0
  149. package/tests/gentle-agents.test.ts +1047 -252
  150. package/tests/gentle-ai-binary.test.ts +3 -3
  151. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  152. package/tests/gentle-ai-installer.test.ts +68 -54
  153. package/tests/gentle-ai-renderer.test.ts +487 -8
  154. package/tests/gentle-ai.test.ts +288 -65
  155. package/tests/gentle-card-text.ts +2 -1
  156. package/tests/gentle-shell-bin.test.ts +651 -114
  157. package/tests/gentle-shell-launcher.test.ts +99 -52
  158. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  159. package/tests/gentle-shell.test.ts +3839 -187
  160. package/tests/gentle-stats.test.ts +152 -0
  161. package/tests/gentle-theme.test.ts +4 -1
  162. package/tests/gentle-todo.test.ts +80 -8
  163. package/tests/history-atomic-write.test.ts +57 -0
  164. package/tests/history-capture-policy.test.ts +102 -0
  165. package/tests/history-command-registration.test.ts +164 -0
  166. package/tests/history-dedupe-entries.test.ts +123 -0
  167. package/tests/history-delete-backfill.test.ts +190 -0
  168. package/tests/history-delete-confirm.test.ts +460 -0
  169. package/tests/history-dispatch.test.ts +180 -0
  170. package/tests/history-drain-hidden.test.ts +110 -0
  171. package/tests/history-drain-order.test.ts +98 -0
  172. package/tests/history-expanded-globals.test.ts +62 -0
  173. package/tests/history-gc.test.ts +832 -0
  174. package/tests/history-header-layout.test.ts +265 -0
  175. package/tests/history-hide-prompts.test.ts +275 -0
  176. package/tests/history-lazy-windowing.test.ts +508 -0
  177. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  178. package/tests/history-load-shared-history.test.ts +53 -0
  179. package/tests/history-max-results-cap.test.ts +76 -0
  180. package/tests/history-multi-reader.test.ts +203 -0
  181. package/tests/history-off-path.test.ts +170 -0
  182. package/tests/history-openflow-integration.test.ts +173 -0
  183. package/tests/history-overlay-margin.test.ts +326 -0
  184. package/tests/history-preview-layout.test.ts +93 -0
  185. package/tests/history-registry.test.ts +143 -0
  186. package/tests/history-scope-delete.test.ts +411 -0
  187. package/tests/history-search-caret-keys.test.ts +142 -0
  188. package/tests/history-seed-bootstrap.test.ts +170 -0
  189. package/tests/history-seed-regen.test.ts +129 -0
  190. package/tests/history-selector-windowing.test.ts +94 -0
  191. package/tests/history-session-scan-directory.test.ts +87 -0
  192. package/tests/history-session-scan-extract.test.ts +583 -0
  193. package/tests/history-session-writer.test.ts +351 -0
  194. package/tests/history-store-paths.test.ts +79 -0
  195. package/tests/history-tombstone-exact.test.ts +139 -0
  196. package/tests/history-wheel-mouse.test.ts +242 -0
  197. package/tests/inprocess-reviewer.test.ts +29 -19
  198. package/tests/issue-creation-skill.test.ts +61 -0
  199. package/tests/model-routing-authority.test.ts +16 -0
  200. package/tests/nan-provider.test.ts +471 -0
  201. package/tests/native-review-capability-contract.test.ts +7 -1
  202. package/tests/native-review-cli.test.ts +6 -120
  203. package/tests/native-review-parity-runtime.test.ts +100 -3
  204. package/tests/odd-integration.test.ts +67 -0
  205. package/tests/odd-phase-inference.test.ts +213 -0
  206. package/tests/odd-phase-loader.test.ts +253 -0
  207. package/tests/odd-phase.test.ts +307 -0
  208. package/tests/odd-routing-canonical-ratchet.test.ts +11 -6
  209. package/tests/odd-routing-contract.test.ts +86 -35
  210. package/tests/orchestrator-budget.test.ts +14 -39
  211. package/tests/orchestrator-rdd-ownership.test.ts +3 -3
  212. package/tests/overlay-repaint.test.ts +74 -0
  213. package/tests/package-manifest.test.ts +252 -115
  214. package/tests/packed-runner-owned-path.test.ts +46 -0
  215. package/tests/persona-single-channel.test.ts +6 -6
  216. package/tests/provider-defect-handoff.test.ts +3 -11
  217. package/tests/quiet-bash-runtime.test.ts +76 -0
  218. package/tests/quiet-tool-rendering.test.ts +409 -184
  219. package/tests/rdd-aware-verification-contract.test.ts +76 -1
  220. package/tests/rdd-status-line.test.ts +9 -4
  221. package/tests/resume-hint-extension.test.ts +122 -0
  222. package/tests/review-agent-end-preflight.test.ts +176 -12
  223. package/tests/review-candidate-owner-retry.test.ts +22 -1
  224. package/tests/review-candidate-view.test.ts +298 -0
  225. package/tests/review-contract-prompt.test.ts +108 -43
  226. package/tests/review-controller-lock-status.test.ts +0 -1
  227. package/tests/review-controller-native-routing.test.ts +611 -5
  228. package/tests/review-controller-workspace-root.test.ts +163 -4
  229. package/tests/review-host-relay-routing.test.ts +338 -2
  230. package/tests/review-integration-v2-forward.test.ts +200 -0
  231. package/tests/review-ledger-contract.test.ts +10 -34
  232. package/tests/review-reminder-receipt.test.ts +47 -1
  233. package/tests/review-risk-assessment.test.ts +498 -6
  234. package/tests/review-sidebar-state.test.ts +402 -0
  235. package/tests/run-test-suite.test.ts +124 -0
  236. package/tests/runtime-harness.mjs +145 -786
  237. package/tests/runtime-metrics-children.test.ts +16 -23
  238. package/tests/selection-engine.test.ts +421 -0
  239. package/tests/session-messaging-grants.test.ts +255 -0
  240. package/tests/session-worktree-registry.test.ts +77 -0
  241. package/tests/shell-bar.test.ts +382 -1
  242. package/tests/shell-card.test.ts +353 -1
  243. package/tests/shell-changes-view.test.ts +52 -0
  244. package/tests/shell-prompt.test.ts +94 -2
  245. package/tests/shell-sidebar-layout.test.ts +325 -21
  246. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  247. package/tests/shell-todo.test.ts +87 -1
  248. package/tests/shell-usage-view.test.ts +27 -0
  249. package/tests/shell-usage.test.ts +73 -0
  250. package/tests/skill-registry.test.ts +50 -1
  251. package/tests/startup-banner.test.ts +130 -2
  252. package/tests/stats-collector.test.ts +195 -0
  253. package/tests/stats-view.test.ts +202 -0
  254. package/tests/telemetry-trigger.test.ts +81 -20
  255. package/tests/theme-customization.test.ts +72 -0
  256. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  257. package/tests/vim-editor-adapter.test.ts +804 -0
  258. package/tests/vim-normal-engine.test.ts +101 -0
  259. package/tests/vim-operator-engine.test.ts +215 -0
  260. package/tests/vim-policy.test.ts +19 -0
  261. package/tests/vim-visual-engine.test.ts +52 -0
  262. package/tests/visual-customization-policy.test.ts +110 -0
  263. package/tests/visual-customize-view.test.ts +418 -0
  264. package/tests/visual-profiles.test.ts +87 -0
  265. package/tests/yolo-customize.test.ts +256 -0
  266. package/tests/yolo-mode-runtime.test.ts +161 -0
  267. package/tests/yolo-mode.test.ts +261 -0
  268. package/tests/yolo-session-policy.test.ts +59 -0
  269. package/themes/Gentle.json +2 -1
  270. package/themes/Gentleman-Cute.json +2 -1
  271. package/themes/Gentleman-Sexy.json +2 -1
  272. package/assets/agents/sdd-apply.md +0 -159
  273. package/assets/agents/sdd-archive.md +0 -228
  274. package/assets/agents/sdd-design.md +0 -49
  275. package/assets/agents/sdd-explore.md +0 -48
  276. package/assets/agents/sdd-init.md +0 -56
  277. package/assets/agents/sdd-onboard.md +0 -52
  278. package/assets/agents/sdd-proposal.md +0 -64
  279. package/assets/agents/sdd-remediate.md +0 -37
  280. package/assets/agents/sdd-research.md +0 -49
  281. package/assets/agents/sdd-spec.md +0 -192
  282. package/assets/agents/sdd-status.md +0 -54
  283. package/assets/agents/sdd-tasks.md +0 -108
  284. package/assets/agents/sdd-verify.md +0 -124
  285. package/assets/chains/sdd-full.chain.md +0 -83
  286. package/assets/chains/sdd-plan.chain.md +0 -56
  287. package/assets/chains/sdd-verify.chain.md +0 -43
  288. package/assets/sdd-orchestrator-workflow.md +0 -319
  289. package/assets/support/sdd-status-contract.md +0 -77
  290. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  291. package/extensions/sdd-init.ts +0 -816
  292. package/lib/openspec-deltas.ts +0 -156
  293. package/lib/sdd-preflight.ts +0 -1066
  294. package/lib/sdd-research-capabilities.ts +0 -94
  295. package/lib/sdd-status.ts +0 -26
  296. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  297. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  298. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  299. package/tests/openspec-deltas.test.ts +0 -209
  300. package/tests/sdd-agent-tools.test.ts +0 -156
  301. package/tests/sdd-archive-replay.test.ts +0 -82
  302. package/tests/sdd-classical-continuation.test.ts +0 -74
  303. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  304. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  305. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  306. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  307. package/tests/sdd-odd-integration.test.ts +0 -33
  308. package/tests/sdd-optional-research.test.ts +0 -124
  309. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  310. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  311. package/tests/sdd-preflight.test.ts +0 -541
  312. package/tests/sdd-research-capabilities.test.ts +0 -114
  313. package/tests/sdd-research-live.test.ts +0 -241
  314. package/tests/sdd-selection-transport.test.ts +0 -653
  315. package/tests/sdd-status.test.ts +0 -9
  316. package/tests/sdd-task-truth.test.ts +0 -43
@@ -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,32 @@ 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.equal(hooks.has("input"), false, "retired SDD slash input must not be intercepted");
363
394
  assert.ok(hooks.has("before_agent_start"), "missing before_agent_start hook");
364
395
  assert.ok(hooks.has("tool_call"), "missing tool_call hook");
365
- for (const toolName of ["read", "bash", "grep", "find", "ls", "edit", "write"]) {
396
+ for (const toolName of ["read", "grep", "find", "ls", "edit", "write"]) {
366
397
  assert.ok(tools.has(toolName), `missing quiet built-in tool renderer ${toolName}`);
367
398
  }
399
+ const codemode = tools.get("codemode");
400
+ assert.ok(codemode, "quiet-tools must decorate the upstream codemode registration");
401
+ assert.equal(codemode.defaultActive, false, "rendering must not activate codemode");
402
+ assert.equal(codemode.renderShell, "self");
403
+ assert.equal(typeof codemode.renderCall, "function");
404
+ assert.equal(typeof codemode.renderResult, "function");
405
+ assert.equal(typeof codemode.prepareLoadout, "function", "upstream loadout policy must survive decoration");
406
+ assert.deepEqual(pi.getActiveTools(), ["read", "bash", "edit", "write"], "card registration must not change tool activation");
368
407
  assert.ok(tools.has("gentle_review"), "missing registered bounded review controller tool");
369
408
  assert.ok(tools.has("gentle_review_scope"), "missing registered bounded review scope tool");
370
409
  assert.deepEqual(
@@ -389,6 +428,22 @@ async function run() {
389
428
  [],
390
429
  "declared extension directory must load without invalid helper modules",
391
430
  );
431
+ assert.ok(
432
+ discovered.extensions.some((extension) => extension.resolvedPath.endsWith(join("extensions", "nan-provider.ts"))),
433
+ "declared extension directory must discover the NaN provider",
434
+ );
435
+
436
+ const nativeNan = discovered.runtime.pendingNativeProviderRegistrations
437
+ .find((entry) => entry.provider.id === "nan")?.provider;
438
+ assert.ok(nativeNan, "actual Pi loader must queue native NaN registration");
439
+ assert.ok(!discovered.runtime.pendingProviderRegistrations.some((entry) => entry.name === "nan"),
440
+ "NaN must not fall back to the legacy empty-key login route");
441
+ await assert.rejects(nativeNan.auth.apiKey.login({
442
+ signal: new AbortController().signal, prompt: async () => "", notify() {},
443
+ }), /non-empty/);
444
+ assert.deepEqual(await nativeNan.auth.apiKey.login({
445
+ signal: new AbortController().signal, prompt: async () => " synthetic-loader-key ", notify() {},
446
+ }), { type: "api_key", key: "synthetic-loader-key" });
392
447
 
393
448
  // orchestrator-lazy-diet: Pi Subagent Model Routing detail (the "do not
394
449
  // pass the `model` parameter by default" / SDD-model-assignment-scoping
@@ -400,62 +455,62 @@ async function run() {
400
455
  const promptCwd = await tempWorkspace();
401
456
  try {
402
457
  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/);
458
+ // gentle-shell#1485: pi-claude-bridge drops a handler-returned systemPrompt
459
+ // and forwards only structured systemPromptOptions, so the harness must
460
+ // land in appendSystemPrompt and the hook must never return a replacement.
461
+ const promptEvent = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
462
+ const promptResult = await promptHook(promptEvent, createCtx(promptCwd));
463
+ assert.equal(promptResult, undefined, "before_agent_start must not return a replacement systemPrompt");
464
+ assert.equal(promptEvent.systemPrompt, "base", "the original systemPrompt field must be left untouched");
465
+ const promptAppended = promptEvent.systemPromptOptions.appendSystemPrompt;
466
+ assert.match(promptAppended, /el Gentleman/);
467
+ assert.match(promptAppended, /Organic Driven Development/);
468
+ assert.doesNotMatch(promptAppended, /## SDD Research Capabilities/);
469
+ assert.match(promptAppended, /review execution contract/);
470
+ assert.doesNotMatch(await readFile(join(ROOT, "extensions", "gentle-ai.ts"), "utf8"), /readCommandSddStatus/);
471
+ assert.match(promptAppended + delegationDetail, /do not pass the `model` parameter by default/);
472
+ assert.doesNotMatch(promptAppended, /Every Agent tool call MUST include `model`/);
415
473
  assert.ok(
416
- promptResult.systemPrompt.includes(
474
+ promptAppended.includes(
417
475
  `Package assets root: \`${join(ROOT, "assets")}\`. Lazy asset paths below are relative to this root.`,
418
476
  ),
419
477
  "parent prompt must declare the one absolute root for relative lazy asset paths",
420
478
  );
421
- assert.match(promptResult.systemPrompt, /`sdd-orchestrator-workflow\.md`/);
422
479
  assert.doesNotMatch(
423
- promptResult.systemPrompt,
480
+ promptAppended,
424
481
  new RegExp(ambientTestAssetsDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")),
425
482
  "normal runtime must ignore ambient GENTLE_PI_TEST_ASSETS_DIR",
426
483
  );
427
- assert.doesNotMatch(promptResult.systemPrompt, /\{\{GENTLE_PI_SDD_WORKFLOW_PATH\}\}/);
428
484
  delete process.env.GENTLE_PI_TEST_ASSETS_DIR;
429
485
  await rm(ambientTestAssetsDir, { recursive: true, force: true });
430
486
  await writeFile(
431
487
  join(globalConfigHome, "persona.json"),
432
488
  '{"mode":"neutral"}\n',
433
489
  );
434
- const neutralPromptResult = await promptHook({ systemPrompt: "base" }, createCtx(promptCwd));
435
- assert.match(neutralPromptResult.systemPrompt, /Do not use slang or regional expressions/);
490
+ const neutralPromptEvent = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
491
+ const neutralPromptResult = await promptHook(neutralPromptEvent, createCtx(promptCwd));
492
+ assert.equal(neutralPromptResult, undefined, "before_agent_start must not return a replacement systemPrompt");
493
+ const neutralAppended = neutralPromptEvent.systemPromptOptions.appendSystemPrompt;
494
+ assert.match(neutralAppended, /Do not use slang or regional expressions/);
436
495
  assert.doesNotMatch(
437
- neutralPromptResult.systemPrompt,
496
+ neutralAppended,
438
497
  /When the user writes Spanish, answer in natural Rioplatense Spanish with voseo/,
439
498
  "neutral persona prompt must not include unconditional voseo instructions after reload",
440
499
  );
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
- );
500
+ const subagentEvent = { agentName: "worker", systemPrompt: "worker base", systemPromptOptions: { appendSystemPrompt: "" } };
501
+ const subagentPromptResult = await promptHook(subagentEvent, createCtx(promptCwd));
502
+ assert.equal(subagentPromptResult, undefined, "before_agent_start must not return a replacement systemPrompt");
503
+ assert.equal(subagentEvent.systemPromptOptions.appendSystemPrompt, "", "a named agent gets nothing appended");
451
504
  await mkdir(join(promptCwd, ".pi", "gentle-ai"), { recursive: true });
452
505
  await writeFile(
453
506
  join(promptCwd, ".pi", "gentle-ai", "persona.json"),
454
507
  '{"mode":"gentleman"}\n',
455
508
  );
456
- const localOverridePromptResult = await promptHook({ systemPrompt: "base" }, createCtx(promptCwd));
509
+ const localOverrideEvent = { systemPrompt: "base", systemPromptOptions: { appendSystemPrompt: "" } };
510
+ const localOverridePromptResult = await promptHook(localOverrideEvent, createCtx(promptCwd));
511
+ assert.equal(localOverridePromptResult, undefined, "before_agent_start must not return a replacement systemPrompt");
457
512
  assert.match(
458
- localOverridePromptResult.systemPrompt,
513
+ localOverrideEvent.systemPromptOptions.appendSystemPrompt,
459
514
  /When the user writes Spanish, answer in natural Rioplatense Spanish with voseo/,
460
515
  );
461
516
  const personaCtx = createCtx(promptCwd, true);
@@ -470,54 +525,6 @@ async function run() {
470
525
  '{\n "mode": "neutral"\n}\n',
471
526
  );
472
527
  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
528
  } finally {
522
529
  await rm(promptCwd, { recursive: true, force: true });
523
530
  }
@@ -606,42 +613,6 @@ async function run() {
606
613
  "a writer may dispatch when context carries narrow task-scoped repository-relative paths",
607
614
  );
608
615
 
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
616
  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
617
  const canonicalFrozenFindingRows = [JSON.parse(canonicalFrozenFindingRow)];
647
618
  const canonicalFrozenLedgerHash = canonicalHash(canonicalFrozenFindingRows);
@@ -899,7 +870,7 @@ async function run() {
899
870
  registry.bindCurrent({ token: view.token, lineageId: "harness-candidate-drift", selectedLenses: ["review-risk"] });
900
871
 
901
872
  const dispatchPi = createPi();
902
- createGentleAiExtension({ candidateViews: registry })(dispatchPi.pi);
873
+ createGentleAiExtension({ ...fixtureDependencies, candidateViews: registry })(dispatchPi.pi);
903
874
  const dispatchToolHook = dispatchPi.hooks.get("tool_call")[0];
904
875
 
905
876
  // The contributor edits the tracked file strictly after the candidate
@@ -1030,24 +1001,9 @@ async function run() {
1030
1001
  for (const handler of hooks.get("session_start")) {
1031
1002
  await handler({ reason: "startup" }, createCtx(noUiCwd, false));
1032
1003
  }
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
1004
  for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
1047
1005
  const ctx = createCtx(noUiCwd, true);
1048
1006
  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
1007
  }
1052
1008
  // gentle-pi#311 P5: the Pi-authored adversarial role agents are retired;
1053
1009
  // installation must not (re)create them.
@@ -1071,19 +1027,10 @@ async function run() {
1071
1027
  );
1072
1028
  assert.match(installedRiskSource, /exactly once against the supplied `initial_review_tree`/);
1073
1029
  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
1030
  const managedAssetsManifestPath = join(globalAgentHome, "gentle-ai", "managed-assets.json");
1079
1031
  const managedAssetsManifest = JSON.parse(
1080
1032
  await readFile(managedAssetsManifestPath, "utf8"),
1081
1033
  );
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
1034
  const samePathUserRefuter = [
1088
1035
  "---",
1089
1036
  "name: review-refuter",
@@ -1106,60 +1053,25 @@ async function run() {
1106
1053
  await mkdir(join(globalAgentHome, "subagents"), { recursive: true });
1107
1054
  const userRefuterOverride = join(globalAgentHome, "subagents", "review-refuter.md");
1108
1055
  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
1056
  await writeFile(
1114
1057
  managedAssetsManifestPath,
1115
1058
  JSON.stringify(managedAssetsManifest, null, 2),
1116
1059
  );
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
1060
  const projectRefuterOverride = join(noUiCwd, ".pi", "agents", "review-refuter.md");
1061
+ await mkdir(dirname(projectRefuterOverride), { recursive: true });
1120
1062
  await writeFile(projectRefuterOverride, "project refuter override must stay\n");
1121
1063
  for (const handler of hooks.get("session_start")) {
1122
1064
  await handler({ reason: "startup" }, createCtx(noUiCwd, false));
1123
1065
  }
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
1066
  assert.equal(existsSync(retiredManagedValidatorPath), false, "review retirement still runs at startup");
1133
1067
  const driftCtx = createCtx(noUiCwd, true);
1134
1068
  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
1069
  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
1070
  assert.equal(
1144
1071
  await readFile(installedRefuterPath, "utf8"),
1145
1072
  samePathUserRefuter,
1146
1073
  "session refresh must preserve a same-path user-authored agent byte-for-byte",
1147
1074
  );
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
1075
  assert.equal(
1164
1076
  await readFile(projectRefuterOverride, "utf8"),
1165
1077
  "project refuter override must stay\n",
@@ -1193,423 +1105,10 @@ async function run() {
1193
1105
  await rm(noUiCwd, { recursive: true, force: true });
1194
1106
  }
1195
1107
 
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"]) {
1108
+ for (const owner of ["delegation", "review"]) {
1609
1109
  const fixture = await tempWorkspace();
1610
1110
  const agentHome = join(fixture, "agent-home");
1611
1111
  const representatives = {
1612
- sdd: "sdd-apply.md",
1613
1112
  delegation: "gentle-ai-worker.md",
1614
1113
  review: "review-risk.md",
1615
1114
  };
@@ -1658,7 +1157,7 @@ async function run() {
1658
1157
  assert.equal(existsSync(join(agentHome, "subagents.json")), false,
1659
1158
  "owner installers must not apply model routing");
1660
1159
  await commands.get("gentle:status").handler("", ctx);
1661
- const label = owner === "sdd" ? "SDD" : owner;
1160
+ const label = owner;
1662
1161
  assert.ok(ctx.ui.notifications.at(-1).message.includes(`Global ${label} user overrides: 1 file(s)`));
1663
1162
  } finally {
1664
1163
  process.env.GENTLE_PI_CONFIG_HOME = globalConfigHome;
@@ -1671,153 +1170,60 @@ async function run() {
1671
1170
  try {
1672
1171
  process.env.GENTLE_PI_AGENT_HOME = join(repairFixture, "agent-home");
1673
1172
  const ctx = createCtx(repairFixture, true);
1674
- await commands.get("gentle:install-sdd").handler("", ctx);
1675
1173
  for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
1676
1174
  await commands.get(diagnostic).handler("", ctx);
1677
1175
  const message = ctx.ui.notifications.at(-1).message;
1678
- assert.match(message, /\/gentle:install-delegation --force/,
1176
+ assert.match(message, /Global delegation assets stale: [1-9]\d* file\(s\).*\/gentle:install-delegation --force/,
1679
1177
  `${diagnostic} must provide a repair for missing delegation assets`);
1680
- assert.match(message, /\/gentle:install-review --force/,
1178
+ assert.match(message, /Global review assets stale: [1-9]\d* file\(s\).*\/gentle:install-review --force/,
1681
1179
  `${diagnostic} must provide a repair for missing review assets`);
1682
1180
  assert.doesNotMatch(message, /install-sdd --force/);
1683
1181
  }
1182
+ await commands.get("gentle:install-delegation").handler("", ctx);
1183
+ await commands.get("gentle:install-review").handler("", ctx);
1684
1184
  const manifest = JSON.parse(await readFile(join(process.env.GENTLE_PI_AGENT_HOME, "gentle-ai", "managed-assets.json"), "utf8"));
1685
1185
  for (const key of Object.keys(manifest.assets)) {
1686
1186
  await rm(join(process.env.GENTLE_PI_AGENT_HOME, key));
1687
1187
  }
1688
1188
  for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
1689
1189
  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");
1190
+ const message = ctx.ui.notifications.at(-1).message;
1191
+ assert.match(message, /Global delegation assets stale: [1-9]\d* file\(s\).*\/gentle:install-delegation --force/);
1192
+ assert.match(message, /Global review assets stale: [1-9]\d* file\(s\).*\/gentle:install-review --force/);
1193
+ assert.doesNotMatch(message, /install-sdd --force|on demand/,
1194
+ "managed installation evidence must survive missing delegation and review files");
1693
1195
  }
1694
1196
  } finally {
1695
1197
  process.env.GENTLE_PI_AGENT_HOME = globalAgentHome;
1696
1198
  await rm(repairFixture, { recursive: true, force: true });
1697
1199
  }
1698
1200
 
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
1201
  const staleAssetsCwd = await tempWorkspace();
1765
1202
  const previousAgentHome = process.env.GENTLE_PI_AGENT_HOME;
1766
1203
  const previousHome = process.env.HOME;
1767
1204
  const previousUserProfile = process.env.USERPROFILE;
1768
1205
  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");
1206
+ process.env.GENTLE_PI_AGENT_HOME = join(staleAssetsCwd, "agent-home");
1207
+ process.env.HOME = staleAssetsCwd;
1208
+ process.env.USERPROFILE = staleAssetsCwd;
1806
1209
  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
1210
  await commands.get("gentle:doctor").handler("", ctx);
1811
1211
  assert.match(ctx.ui.notifications.at(-1).message, /el Gentleman doctor/);
1812
1212
  assert.match(ctx.ui.notifications.at(-1).message, /Sensitive-path guard active/);
1213
+ for (const diagnostic of ["gentle:status", "gentle:doctor"]) {
1214
+ await commands.get(diagnostic).handler("", ctx);
1215
+ const message = ctx.ui.notifications.at(-1).message;
1216
+ assert.match(message, /Organic Driven Development \(ODD\): active/);
1217
+ assert.doesNotMatch(message, /OpenSpec|openspec\/config\.yaml|SDD/);
1218
+ }
1813
1219
  pi.setActiveTools([{ name: "engram.mem_save" }]);
1814
1220
  await commands.get("gentle:doctor").handler("", ctx);
1815
1221
  assert.match(ctx.ui.notifications.at(-1).message, /Engram memory tools active/);
1816
1222
  pi.setActiveTools([{ name: "engram_mem_save" }]);
1817
1223
  await commands.get("gentle:doctor").handler("", ctx);
1818
1224
  assert.match(ctx.ui.notifications.at(-1).message, /Engram memory tools not active in this session/);
1819
- pi.setActiveTools(["read", "bash", "edit", "write"]);
1820
1225
  } finally {
1226
+ pi.setActiveTools(["read", "bash", "edit", "write"]);
1821
1227
  if (previousAgentHome === undefined) delete process.env.GENTLE_PI_AGENT_HOME;
1822
1228
  else process.env.GENTLE_PI_AGENT_HOME = previousAgentHome;
1823
1229
  if (previousHome === undefined) delete process.env.HOME;
@@ -1827,59 +1233,6 @@ async function run() {
1827
1233
  await rm(staleAssetsCwd, { recursive: true, force: true });
1828
1234
  }
1829
1235
 
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
1236
  const legacyModelsCwd = await tempWorkspace();
1884
1237
  try {
1885
1238
  await mkdir(join(legacyModelsCwd, ".pi", "agents"), { recursive: true });
@@ -2240,7 +1593,7 @@ async function run() {
2240
1593
  { provider: "evil\u001b]52;c;Zm9v\u0007", id: "model" },
2241
1594
  ];
2242
1595
  ctx.ui.custom = (factory) => {
2243
- const panel = factory(null, null, null, () => undefined);
1596
+ const panel = factory({ terminal: { rows: 24 } }, null, null, () => undefined);
2244
1597
  const initialLines = panel.render(120);
2245
1598
  const plainInitialLines = initialLines.map(stripAnsi);
2246
1599
  assert.ok(
@@ -2248,8 +1601,8 @@ async function run() {
2248
1601
  "model panel should render inside a bordered card",
2249
1602
  );
2250
1603
  assert.ok(
2251
- initialLines.length <= 20,
2252
- "long model agent list should fit within a 24-row terminal 85% overlay budget",
1604
+ initialLines.length === 24,
1605
+ "model panel should fill the 24-row terminal like /gentle:profiles",
2253
1606
  );
2254
1607
  assert.ok(
2255
1608
  plainInitialLines.some((line) => /↓ \d+ more agent\(s\)/.test(line)),
@@ -2273,8 +1626,8 @@ async function run() {
2273
1626
  const scrolledLines = panel.render(120);
2274
1627
  const plainScrolledLines = scrolledLines.map(stripAnsi);
2275
1628
  assert.ok(
2276
- scrolledLines.length <= 20,
2277
- "scrolled model agent list should stay within the overlay height budget",
1629
+ scrolledLines.length === 24,
1630
+ "scrolled model agent list should keep filling the terminal",
2278
1631
  );
2279
1632
  assert.ok(
2280
1633
  plainScrolledLines.some((line) => /↑ \d+ more agent\(s\)/.test(line)),
@@ -2284,8 +1637,8 @@ async function run() {
2284
1637
  const bottomLines = panel.render(120);
2285
1638
  const plainBottomLines = bottomLines.map(stripAnsi);
2286
1639
  assert.ok(
2287
- bottomLines.length <= 20,
2288
- "bottom model agent list should stay within the overlay height budget",
1640
+ bottomLines.length === 24,
1641
+ "bottom model agent list should keep filling the terminal",
2289
1642
  );
2290
1643
  assert.ok(
2291
1644
  plainBottomLines.some((line) => line.includes("▸ ← Back")),
@@ -2532,4 +1885,10 @@ async function run() {
2532
1885
  run().catch((error) => {
2533
1886
  console.error(error);
2534
1887
  process.exitCode = 1;
1888
+ }).finally(async () => {
1889
+ for (const [key, value] of previousFixtureEnvironment) {
1890
+ if (value === undefined) delete process.env[key];
1891
+ else process.env[key] = value;
1892
+ }
1893
+ for (const root of ownedFixtureRoots) await rm(root, { recursive: true, force: true });
2535
1894
  });