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
@@ -0,0 +1,240 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { appendSystemPromptOnce, type AppendableSystemPromptOptions } from "./append-system-prompt.ts";
3
+ import { captureReviewSessionIdentity, sameReviewSessionIdentity, type ReviewSessionIdentity } from "./review-session-standing-permission.ts";
4
+
5
+ export const YOLO_STATUS_KEY = "gentle:yolo";
6
+ export const YOLO_STATUS_TEXT = "🚀 YOLO ON 🔥 — destructive confirmations remain";
7
+ export const YOLO_DIRECTIVE = `<gentle-yolo-session>
8
+ YOLO session standing permission is ON, explicitly activated by the human for this live primary session and Git clone.
9
+ Qualify the default commit, push and PR confirmation clauses: for ordinary already-scoped implementation, checks, commits, non-force pushes and PR creation, this activation supplies standing permission instead of repeated permission questions. Make ordinary reversible implementation choices without needless interviews.
10
+ This does NOT override explicit human restrictions, repository policy, project trust, configured confirmations or blocks, or the authorized task scope. Ask before scope expansion, privacy-sensitive disclosure, genuinely unresolved consequential product choices, or ambiguous destinations or credentials. Never invent destinations or credentials.
11
+ Destructive/data-loss confirmations remain mandatory and independent. Never auto-answer ask_user tools, provider consent, maintenance/recovery authorization or opaque-token decisions. Review standing permission and RDD are unchanged. Children remain bounded by their delegated task; they receive no independent delivery grant or YOLO inheritance.
12
+ </gentle-yolo-session>`;
13
+
14
+ /** Separate from review grants; no registry, entries, configuration or environment enable switch. */
15
+ export class YoloSessionPolicy {
16
+ private grant: ReviewSessionIdentity | undefined;
17
+ private generation = 0;
18
+
19
+ get epoch(): number { return this.generation; }
20
+ get enabled(): boolean { return this.grant !== undefined; }
21
+
22
+ reset(): void {
23
+ this.generation += 1;
24
+ this.grant = undefined;
25
+ }
26
+
27
+ set(enabled: boolean, identity: ReviewSessionIdentity | undefined, expectedEpoch = this.epoch): boolean {
28
+ if (expectedEpoch !== this.epoch) return false;
29
+ this.reset();
30
+ if (!enabled) return true;
31
+ if (identity === undefined) return false;
32
+ this.grant = identity;
33
+ return true;
34
+ }
35
+
36
+ active(identity: ReviewSessionIdentity | undefined): boolean {
37
+ if (!this.grant) return false;
38
+ if (identity !== undefined && sameReviewSessionIdentity(this.grant, identity)) return true;
39
+ this.reset();
40
+ return false;
41
+ }
42
+ }
43
+
44
+ /** Remove our exact block as well as adding it: bridge options can be reused. */
45
+ export function updateYoloPrompt(options: AppendableSystemPromptOptions | null | undefined, active: boolean): void {
46
+ if (!options) return;
47
+ options.appendSystemPrompt = (options.appendSystemPrompt ?? "")
48
+ .replace(`\n\n${YOLO_DIRECTIVE}`, "").replace(YOLO_DIRECTIVE, "");
49
+ if (active) appendSystemPromptOnce(options, YOLO_DIRECTIVE);
50
+ }
51
+
52
+ const HUMAN_ACTION = { on: "on", off: "off", toggle: "" } as const;
53
+ type HumanAction = (typeof HUMAN_ACTION)[keyof typeof HUMAN_ACTION];
54
+ export const YOLO_DISPLAY = { on: "ON", off: "OFF", unavailable: "UNAVAILABLE" } as const;
55
+ export type YoloDisplay = (typeof YOLO_DISPLAY)[keyof typeof YOLO_DISPLAY];
56
+
57
+ /** Private trusted-extension discovery, never a model/tool or serialized authority event. */
58
+ const HOST_UI_CHANNEL = "gentle:yolo:host-ui";
59
+ const DISCOVERY_TIMEOUT_MS = 100;
60
+ export interface YoloUiAdapter {
61
+ read(): Promise<YoloDisplay>;
62
+ toggle(): Promise<void>;
63
+ observe(refresh: () => void): () => void;
64
+ dispose(): void;
65
+ }
66
+ interface HostUiRequest {
67
+ context: ExtensionContext;
68
+ respond(adapter: YoloUiAdapter): void;
69
+ }
70
+ function isUiAdapter(value: unknown): value is YoloUiAdapter {
71
+ if (!value || typeof value !== "object") return false;
72
+ const adapter = value as Partial<YoloUiAdapter>;
73
+ return [adapter.read, adapter.toggle, adapter.observe, adapter.dispose].every(fn => typeof fn === "function");
74
+ }
75
+
76
+ /** emit is void (and listeners may be wrapped async); explicitly bound the callback wait. */
77
+ export function discoverYoloUiAdapter(pi: ExtensionAPI, context: ExtensionContext): Promise<YoloUiAdapter | undefined> {
78
+ return new Promise(resolve => {
79
+ let settled = false;
80
+ const timer = setTimeout(() => { settled = true; resolve(undefined); }, DISCOVERY_TIMEOUT_MS);
81
+ const respond = (value: unknown) => {
82
+ if (!isUiAdapter(value)) return;
83
+ if (settled) { value.dispose(); return; }
84
+ settled = true;
85
+ clearTimeout(timer);
86
+ resolve(value);
87
+ };
88
+ try { pi.events.emit(HOST_UI_CHANNEL, { context, respond } satisfies HostUiRequest); }
89
+ catch { settled = true; clearTimeout(timer); resolve(undefined); }
90
+ });
91
+ }
92
+
93
+ export interface YoloSessionController {
94
+ active(context: ExtensionContext): Promise<boolean>;
95
+ humanAction(action: HumanAction, context: ExtensionContext, current?: () => boolean): Promise<boolean>;
96
+ reset(context: ExtensionContext): void;
97
+ }
98
+
99
+ /** Register only a human slash command, never a tool, flag or persisted setting. */
100
+ export function registerYoloSessionPolicy(pi: ExtensionAPI, env: NodeJS.ProcessEnv): YoloSessionController {
101
+ const policy = new YoloSessionPolicy();
102
+ let ownerGeneration = 0;
103
+ let publishedEpoch = policy.epoch;
104
+ const observers = new Set<() => void>();
105
+ const publish = (context: ExtensionContext, active: boolean): void => {
106
+ // Display is not authority. Widgets survive shells that hide the status rail.
107
+ try { context.ui.setStatus(YOLO_STATUS_KEY, active ? YOLO_STATUS_TEXT : undefined); } catch { /* nonblocking */ }
108
+ try { context.ui.setWidget(YOLO_STATUS_KEY, active ? [YOLO_STATUS_TEXT] : undefined); } catch { /* nonblocking */ }
109
+ // Repeated state reads publish indicators, but only a policy transition
110
+ // wakes observers. active -> publish -> observer -> read thus terminates.
111
+ if (publishedEpoch === policy.epoch) return;
112
+ publishedEpoch = policy.epoch;
113
+ for (const refresh of [...observers]) { try { refresh(); } catch { /* display only */ } }
114
+ };
115
+ const capture = async (context: ExtensionContext): Promise<ReviewSessionIdentity | undefined> => {
116
+ try {
117
+ const cwd = context.cwd;
118
+ const identity = await captureReviewSessionIdentity(context, env);
119
+ // Git lookup awaits: a replacement manager ID must not activate stale scope.
120
+ return cwd === context.cwd && identity?.sessionManager === context.sessionManager &&
121
+ identity?.sessionId === context.sessionManager.getSessionId() ? identity : undefined;
122
+ } catch { return undefined; }
123
+ };
124
+ const active = async (context: ExtensionContext): Promise<boolean> => {
125
+ const epoch = policy.epoch;
126
+ const identity = policy.enabled ? await capture(context) : undefined;
127
+ if (epoch !== policy.epoch) return false;
128
+ const enabled = policy.active(identity);
129
+ publish(context, enabled);
130
+ return enabled;
131
+ };
132
+ // The owning extension calls this from its existing lifecycle hooks, including
133
+ // reload. Keep their cardinality and ordering intact for SDK and legacy hosts.
134
+ const reset = (context: ExtensionContext): void => {
135
+ ownerGeneration += 1;
136
+ policy.reset();
137
+ publish(context, false);
138
+ observers.clear();
139
+ };
140
+ // Binds an interaction to the session that opened it: a reload, replacement,
141
+ // or cwd change while the human is deciding must not act on the new scope.
142
+ const bindCurrent = (context: ExtensionContext, disposed = () => false): (() => boolean) => {
143
+ const generation = ownerGeneration;
144
+ const manager = context.sessionManager, sessionId = manager.getSessionId(), cwd = context.cwd;
145
+ return () => {
146
+ try { return !disposed() && generation === ownerGeneration && manager === context.sessionManager &&
147
+ sessionId === manager.getSessionId() && cwd === context.cwd && context.hasUI; }
148
+ catch { return false; } // SDK contexts throw after runtime replacement.
149
+ };
150
+ };
151
+ // Both entry points execute this operation, including off/pending-on races.
152
+ // Resolves false only when the interaction stopped being current, so the
153
+ // caller can report the discarded choice; a superseding action stays silent.
154
+ const humanAction = async (action: HumanAction, context: ExtensionContext, current = () => true): Promise<boolean> => {
155
+ if (!current()) return false;
156
+ if (action === HUMAN_ACTION.off) {
157
+ policy.reset(); publish(context, false); context.ui.notify("YOLO OFF", "info");
158
+ return true;
159
+ }
160
+ const wasEnabled = policy.enabled;
161
+ policy.reset();
162
+ publish(context, false);
163
+ const epoch = policy.epoch;
164
+ const identity = await capture(context);
165
+ const enable = action === HUMAN_ACTION.on || !wasEnabled;
166
+ if (!current()) return false;
167
+ if (epoch !== policy.epoch) return true;
168
+ const changed = policy.set(enable, identity, epoch);
169
+ const enabled = changed && policy.active(identity);
170
+ publish(context, enabled);
171
+ context.ui.notify(enabled ? YOLO_STATUS_TEXT : enable
172
+ ? "YOLO OFF — activation requires an interactive primary TUI session and an identifiable Git clone."
173
+ : "YOLO OFF", enabled || !enable ? "info" : "warning");
174
+ return true;
175
+ };
176
+ // Legacy minimal hosts can run the command without an inter-extension bus;
177
+ // they simply cannot expose the menu adapter.
178
+ pi.events?.on?.(HOST_UI_CHANNEL, (payload: unknown) => {
179
+ if (!payload || typeof payload !== "object") return;
180
+ const request = payload as Partial<HostUiRequest>;
181
+ if (typeof request.respond !== "function" || !request.context) return;
182
+ const context = request.context;
183
+ try {
184
+ let disposed = false;
185
+ const subscriptions = new Set<() => void>();
186
+ const current = bindCurrent(context, () => disposed);
187
+ const read = async (): Promise<YoloDisplay> => {
188
+ if (!current()) return YOLO_DISPLAY.unavailable;
189
+ const epoch = policy.epoch;
190
+ const identity = await capture(context);
191
+ if (!current()) return YOLO_DISPLAY.unavailable;
192
+ if (epoch !== policy.epoch) return read(); // observe the winning off/on, never an old ON.
193
+ const enabled = policy.active(identity);
194
+ publish(context, enabled);
195
+ return identity ? enabled ? YOLO_DISPLAY.on : YOLO_DISPLAY.off : YOLO_DISPLAY.unavailable;
196
+ };
197
+ request.respond({
198
+ read,
199
+ toggle: async () => { await humanAction(HUMAN_ACTION.toggle, context, current); },
200
+ observe: (refresh) => {
201
+ if (!current()) return () => {};
202
+ subscriptions.add(refresh); observers.add(refresh);
203
+ return () => { subscriptions.delete(refresh); observers.delete(refresh); };
204
+ },
205
+ dispose: () => {
206
+ disposed = true;
207
+ for (const refresh of subscriptions) observers.delete(refresh);
208
+ subscriptions.clear();
209
+ },
210
+ });
211
+ } catch { /* Missing or obsolete trusted host context: discovery expires closed. */ }
212
+ });
213
+ pi.registerCommand("gentle:yolo", {
214
+ description: "Session-only ordinary development/delivery permission (enable|disable|status); no argument opens a menu. Destructive confirmations remain.",
215
+ handler: async (args, context) => {
216
+ let action = args.trim() || "status";
217
+ let current = () => true;
218
+ // Headless callers keep the status fallback: an empty argument never grants.
219
+ if (!args.trim() && context.hasUI && typeof context.ui.select === "function") {
220
+ current = bindCurrent(context);
221
+ const state = await active(context) ? YOLO_DISPLAY.on : YOLO_DISPLAY.off;
222
+ const selected = await context.ui.select(`🚀 Gentle YOLO 🔥 — full speed, destructive actions still ask (current: ${state})`, ["enable", "disable", "status"]);
223
+ if (selected === undefined) return;
224
+ action = selected;
225
+ }
226
+ if (action !== "enable" && action !== "disable" && action !== "status") {
227
+ context.ui.notify("Use /gentle:yolo enable|disable|status. State unchanged.", "warning");
228
+ return;
229
+ }
230
+ if (action === "status") {
231
+ context.ui.notify(await active(context) ? YOLO_STATUS_TEXT : "YOLO OFF", "info");
232
+ return;
233
+ }
234
+ if (await humanAction(action === "enable" ? HUMAN_ACTION.on : HUMAN_ACTION.off, context, current)) return;
235
+ try { context.ui.notify("YOLO unchanged — the session changed while the menu was open.", "warning"); }
236
+ catch { /* SDK contexts throw after runtime replacement. */ }
237
+ },
238
+ });
239
+ return { active, humanAction, reset };
240
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gentle-pi",
3
- "version": "3.7.0",
4
- "description": "Turn Pi into el Gentleman: a senior-architect development harness with SDD/OpenSpec, subagents, strict TDD evidence, review guardrails, and skill discovery.",
3
+ "version": "4.0.0",
4
+ "description": "Turn Pi into el Gentleman: an ODD development harness with focused subagents, configured TDD evidence, native review, and skill discovery.",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
@@ -13,8 +13,7 @@
13
13
  "pi-coding-agent",
14
14
  "gentle-pi",
15
15
  "gentle-ai",
16
- "sdd",
17
- "openspec",
16
+ "odd",
18
17
  "subagents",
19
18
  "tdd",
20
19
  "code-review",
@@ -37,6 +36,7 @@
37
36
  "skills/",
38
37
  "scripts/",
39
38
  "tests/",
39
+ "!tests/fixtures/legacy/sdd-research-v2.5.0.md",
40
40
  "themes/",
41
41
  "README.md"
42
42
  ],
@@ -44,7 +44,7 @@
44
44
  "check:provider-contract": "node scripts/check-provider-contract.mjs",
45
45
  "mirror:odd-routing": "node scripts/mirror-odd-routing.mjs",
46
46
  "postinstall": "node scripts/install-gentle-ai.mjs",
47
- "test": "node --experimental-strip-types --test tests/*.test.ts && pnpm run check:provider-contract && pnpm run test:harness",
47
+ "test": "node scripts/run-test-suite.mjs",
48
48
  "test:harness": "node --experimental-strip-types tests/runtime-harness.mjs",
49
49
  "test:dev-binary": "node --experimental-strip-types --test tests/devbinary/*.devtest.ts",
50
50
  "test:cross-lane": "node tests/crosslane/cross-lane.mjs",
@@ -72,22 +72,34 @@
72
72
  ]
73
73
  },
74
74
  "dependencies": {
75
- "@earendil-works/pi-tui": "0.85.1",
76
75
  "@heyhuynhgiabuu/pi-pretty": "0.6.27"
77
76
  },
78
77
  "peerDependencies": {
79
- "@earendil-works/pi-coding-agent": ">=0.85.1",
78
+ "@earendil-works/pi-coding-agent": ">=0.99.1",
79
+ "@earendil-works/pi-ai": "*",
80
+ "@earendil-works/pi-tui": "*",
80
81
  "typebox": "*"
81
82
  },
82
83
  "peerDependenciesMeta": {
83
84
  "@earendil-works/pi-coding-agent": {
84
85
  "optional": true
86
+ },
87
+ "@earendil-works/pi-ai": {
88
+ "optional": true
89
+ },
90
+ "@earendil-works/pi-tui": {
91
+ "optional": true
85
92
  }
86
93
  },
87
94
  "devDependencies": {
88
- "@earendil-works/pi-coding-agent": "0.85.1",
95
+ "@earendil-works/pi-coding-agent": ">=1.0.0",
96
+ "@earendil-works/pi-ai": ">=1.0.0",
97
+ "@earendil-works/pi-tui": ">=1.0.0",
89
98
  "@types/node": "^24.13.3",
90
99
  "typescript": "^5.9.3"
91
100
  },
101
+ "engines": {
102
+ "node": ">=22.19.0"
103
+ },
92
104
  "packageManager": "pnpm@11.1.1"
93
105
  }
@@ -195,6 +195,16 @@ function linkDir(env , homedir )
195
195
  return env.PI_CODING_AGENT_DIR || join(homedir, ".pi", "agent");
196
196
  }
197
197
 
198
+ // The isolated home replaces PI_CODING_AGENT_DIR for the whole session, so the
199
+ // user's own Pi home travels in this variable for read-only features such as
200
+ // /gentle:stats. An inherited value wins: a gentle-shell launched from inside
201
+ // a Gentle Shell session sees the outer isolated home as PI_CODING_AGENT_DIR.
202
+ export const USER_PI_HOME_ENV = "GENTLE_SHELL_USER_PI_HOME";
203
+
204
+ export function userPiHome(env , homedir ) {
205
+ return env[USER_PI_HOME_ENV] || linkDir(env, homedir);
206
+ }
207
+
198
208
  function isolatedDir(env , homedir ) {
199
209
  return env.GENTLE_SHELL_HOME || join(homedir, ".gentle-shell", "agent");
200
210
  }
@@ -380,7 +390,7 @@ export function missingPiMessage() {
380
390
 
381
391
  // --- pi version gate ---------------------------------------------------------
382
392
 
383
- export const MIN_PI_VERSION = "0.85.1";
393
+ export const MIN_PI_VERSION = "0.99.1";
384
394
 
385
395
 
386
396
 
@@ -875,18 +885,13 @@ export function discoverLooseExtensionEntries(dir , fs )
875
885
 
876
886
 
877
887
 
878
- function packageRootAssetArgs(packageRoot ) {
879
- return ["--theme", join(packageRoot, "themes"), "--skill", join(packageRoot, "skills"), "--prompt-template", join(packageRoot, "prompts")];
880
- }
881
888
 
882
- function packageRootInjectionArgs(packageRoot ) {
883
- return ["-e", packageRoot, ...packageRootAssetArgs(packageRoot)];
884
- }
889
+
885
890
 
886
891
  // Four cases, checked in this order — `piSubcommand` first, then `takeOver`:
887
892
  // - piSubcommand: pi dispatches install/remove/uninstall/update/list/
888
893
  // config/auth on argv[0] before it even parses flags, so any injected
889
- // -e/--theme/--skill/--prompt-template flag ahead of it stops pi from
894
+ // -e flag ahead of it stops pi from
890
895
  // recognising its subcommand at all — this is exactly the observed
891
896
  // 2026-09-22 bug where `gentle-shell install npm:x` opened an
892
897
  // interactive pi session instead of running the package manager. No
@@ -909,7 +914,8 @@ function packageRootInjectionArgs(packageRoot ) {
909
914
  // (R3-001): a loose entry that duplicates an other-package path, or
910
915
  // repeats within looseExtensionEntries itself, is skipped rather than
911
916
  // loaded twice.
912
- // - Not takeOver, no declaration: inject this launcher's own packageRoot,
917
+ // - Not takeOver, no declaration: inject this launcher's own packageRoot
918
+ // once via -e; Pi discovers its extensions, skills, prompts and themes,
913
919
  // exactly as when nothing else in settings loads gentle-pi.
914
920
  // - Not takeOver, with a declaration: no injection at all — the target
915
921
  // settings already load a gentle-pi the launcher accepts as-is (the
@@ -940,9 +946,9 @@ export function buildPiInvocation(input ) {
940
946
  injected.add(input.packageRoot);
941
947
  args.push("-e", input.packageRoot);
942
948
  }
943
- args.push(...packageRootAssetArgs(input.packageRoot));
949
+
944
950
  } else if (input.declaration === undefined) {
945
- args.push(...packageRootInjectionArgs(input.packageRoot));
951
+ args.push("-e", input.packageRoot);
946
952
  }
947
953
 
948
954
  args.push(...input.passthrough);
@@ -950,7 +956,12 @@ export function buildPiInvocation(input ) {
950
956
  return {
951
957
  command: input.runtime.command,
952
958
  args,
953
- env: { ...input.baseEnv, PI_CODING_AGENT_DIR: input.home.dir, GENTLE_PI_AGENT_HOME: input.home.dir },
959
+ env: {
960
+ ...input.baseEnv,
961
+ PI_CODING_AGENT_DIR: input.home.dir,
962
+ GENTLE_PI_AGENT_HOME: input.home.dir,
963
+ [USER_PI_HOME_ENV]: userPiHome(input.baseEnv, input.homedir),
964
+ },
954
965
  };
955
966
  }
956
967
 
@@ -0,0 +1,177 @@
1
+ // Generated by scripts/build-runtime-modules.mjs. Do not edit.
2
+ // Pure logic behind the gentle-shell resume hint. On interactive quit pi
3
+ // prints "To resume this session: pi --session <id>" (interactive-mode
4
+ // formatResumeCommand). Under gentle-shell that command cannot find the
5
+ // session: pi resolves sessions from PI_CODING_AGENT_DIR, which the launcher
6
+ // points at the gentle-shell home, but the hint names the bare `pi` binary
7
+ // and never mentions that directory, so running it looks in ~/.pi/agent.
8
+ //
9
+ // The clean fix belongs in pi (earendil-works/pi#8048, #9750). Until then,
10
+ // gentle-shell appends its own line below pi's, leaving pi's output as is:
11
+ // extensions/resume-hint.ts writes a ResumeHandoff on session_shutdown, and
12
+ // bin/gentle-shell.mjs prints the planResumeHint line after pi exits.
13
+ import { basename, dirname, isAbsolute, join, resolve as resolvePath } from "node:path";
14
+ import { shellQuote } from "./gentle-shell-launcher.mjs";
15
+
16
+ export const RESUME_HANDOFF_ENV = "GENTLE_SHELL_RESUME_HANDOFF";
17
+
18
+ const HINT_LABEL = "To resume in gentle-shell:";
19
+
20
+ // pi's assertValidSessionId charset. The handoff ends up on the terminal, so
21
+ // anything outside it (or any C0/C1 control in a session dir) is refused
22
+ // rather than escaped.
23
+ const SESSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
24
+ const CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/;
25
+
26
+ // The launcher creates <tmpdir>/<RESUME_HANDOFF_DIR_PREFIX>XXXXXX/<RESUME_HANDOFF_FILE>.
27
+ export const RESUME_HANDOFF_DIR_PREFIX = "gentle-shell-resume-";
28
+ export const RESUME_HANDOFF_FILE = "handoff.json";
29
+
30
+ // The extension only writes to a path shaped like the launcher's private
31
+ // handoff, so an inherited or foreign env value cannot aim it at another file.
32
+ export function isResumeHandoffPath(path ) {
33
+ return (
34
+ isAbsolute(path) &&
35
+ basename(path) === RESUME_HANDOFF_FILE &&
36
+ basename(dirname(path)).startsWith(RESUME_HANDOFF_DIR_PREFIX) &&
37
+ !CONTROL_CHARS.test(path)
38
+ );
39
+ }
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+ // Mirror of pi's getDefaultSessionDirPath (core/session-manager), which pi
54
+ // does not export. Kept byte-for-byte so usesDefaultSessionDir agrees.
55
+ export function piDefaultSessionDir(cwd , agentDir ) {
56
+ const resolvedCwd = resolvePath(cwd);
57
+ const safePath = `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
58
+ return join(resolvePath(agentDir), "sessions", safePath);
59
+ }
60
+
61
+
62
+
63
+
64
+
65
+
66
+
67
+
68
+
69
+
70
+
71
+
72
+ // Mirrors the guards in pi's formatResumeCommand: no hint for an
73
+ // unpersisted session or one whose file was never written.
74
+ export function resumeHandoffFromSession(snapshot ) {
75
+ const { sessionId, sessionDir, sessionFile, cwd, launchCwd, agentDir, fileExists } = snapshot;
76
+ if (!sessionFile || !fileExists(sessionFile)) return undefined;
77
+ if (resolvePath(cwd) !== resolvePath(launchCwd)) return { sessionId, sessionFile: resolvePath(sessionFile) };
78
+ if (sessionDir === piDefaultSessionDir(cwd, agentDir)) return { sessionId };
79
+ return { sessionId, sessionDir };
80
+ }
81
+
82
+ export function serializeResumeHandoff(handoff ) {
83
+ return JSON.stringify(handoff);
84
+ }
85
+
86
+ // Tolerant on purpose: a missing, stale, or foreign handoff file just means
87
+ // "print nothing".
88
+ export function parseResumeHandoff(text ) {
89
+ let parsed ;
90
+ try {
91
+ parsed = JSON.parse(text);
92
+ } catch {
93
+ return undefined;
94
+ }
95
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
96
+ const { sessionId, sessionDir, sessionFile } = parsed ;
97
+ if (typeof sessionId !== "string" || !SESSION_ID_PATTERN.test(sessionId)) return undefined;
98
+ if (sessionFile !== undefined) {
99
+ if (sessionDir !== undefined || typeof sessionFile !== "string") return undefined;
100
+ if (!isAbsolute(sessionFile) || !sessionFile.endsWith(".jsonl") || CONTROL_CHARS.test(sessionFile)) return undefined;
101
+ return { sessionId, sessionFile };
102
+ }
103
+ if (sessionDir === undefined) return { sessionId };
104
+ if (typeof sessionDir !== "string" || sessionDir.length === 0 || CONTROL_CHARS.test(sessionDir)) return undefined;
105
+ return { sessionId, sessionDir };
106
+ }
107
+
108
+ // Values that need no quoting in any shell a user might paste the hint into.
109
+ const PLAIN_ARG = /^[A-Za-z0-9_\-.:/\\=]+$/;
110
+ // Characters that stay live inside double quotes: cmd.exe expands %VAR% (and
111
+ // !VAR! under delayed expansion), PowerShell expands $var and `escapes, and
112
+ // an inner " ends the quoted argument in both. A trailing backslash would
113
+ // escape the closing quote under the Windows argv rules. cmd.exe operators
114
+ // are refused too: gentle-shell is installed as a .cmd shim, and PowerShell
115
+ // drops the quotes of a space-free argument when it calls one, so cmd.exe
116
+ // would read & | < > ^ ( ) as operators.
117
+ const WINDOWS_UNQUOTABLE = /["%!$`&|<>^()]|\\$/;
118
+
119
+ // Quotes one argument for the shell the user is likely to paste into:
120
+ // POSIX single quotes elsewhere, double quotes on win32, where cmd.exe and
121
+ // PowerShell do not treat single quotes as quoting. Returns undefined when
122
+ // the value cannot be quoted safely there.
123
+ function quoteArg(value , platform ) {
124
+ if (platform !== "win32") return shellQuote(value);
125
+ if (PLAIN_ARG.test(value)) return value;
126
+ if (WINDOWS_UNQUOTABLE.test(value) || CONTROL_CHARS.test(value)) return undefined;
127
+ return `"${value}"`;
128
+ }
129
+
130
+ // Returns the gentle-shell command that resumes the handed-off session, or
131
+ // undefined when some argument cannot be quoted safely for the platform's
132
+ // shell (the caller then prints no hint and leaves pi's own line alone).
133
+ export function gentleShellResumeCommand(
134
+ handoff ,
135
+ homeFlags ,
136
+ platform ,
137
+ ) {
138
+ const values = [...homeFlags];
139
+ // pi treats a --session value containing a path separator as a file path
140
+ // and opens it directly, whatever the launch directory.
141
+ if (handoff.sessionFile !== undefined) {
142
+ values.push("--session", handoff.sessionFile);
143
+ } else {
144
+ if (handoff.sessionDir !== undefined) values.push("--session-dir", handoff.sessionDir);
145
+ values.push("--session", handoff.sessionId);
146
+ }
147
+ const args = ["gentle-shell"];
148
+ for (const value of values) {
149
+ const quoted = quoteArg(value, platform);
150
+ if (quoted === undefined) return undefined;
151
+ args.push(quoted);
152
+ }
153
+ return args.join(" ");
154
+ }
155
+
156
+
157
+
158
+
159
+
160
+
161
+
162
+
163
+
164
+
165
+
166
+
167
+
168
+ // Returns the line to print after pi exits, or undefined to print nothing.
169
+ // Like pi, it only prints to a TTY, and never after the terminal hung up.
170
+ export function planResumeHint(input ) {
171
+ const { handoff, homeFlags, stdoutIsTTY, terminalHungUp, platform, color } = input;
172
+ if (!handoff || !stdoutIsTTY || terminalHungUp) return undefined;
173
+ const command = gentleShellResumeCommand(handoff, homeFlags, platform);
174
+ if (command === undefined) return undefined;
175
+ const label = color ? `\u001b[2m${HINT_LABEL}\u001b[22m` : HINT_LABEL;
176
+ return `${label} ${command}\n`;
177
+ }