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,6 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { createNanProviderConfig } from "../lib/nan-provider.ts";
3
+
4
+ export default function registerNanProvider(pi: ExtensionAPI): void {
5
+ pi.registerProvider(createNanProviderConfig());
6
+ }
@@ -1,6 +1,5 @@
1
- import type { AgentToolResult, ExtensionAPI } from "@earendil-works/pi-coding-agent";
1
+ import type { AgentToolResult, ExtensionAPI, ExtensionFactory, ToolDefinition } from "@earendil-works/pi-coding-agent";
2
2
  import {
3
- createBashTool,
4
3
  createEditTool,
5
4
  createFindTool,
6
5
  createGrepTool,
@@ -13,11 +12,18 @@ import { Text, type Component } from "@earendil-works/pi-tui";
13
12
  import { homedir } from "node:os";
14
13
  import { isAbsolute } from "node:path";
15
14
  import { resolveGentleAiDevBinaryOverride, type GentleAiDevBinaryOverride } from "../lib/gentle-ai-binary.ts";
15
+ import { GentleAiElapsedTimingLedger } from "../lib/gentle-ai-elapsed-store.ts";
16
16
  import { quietToolsEnabled } from "../lib/quiet-tools-config.ts";
17
+ import { registerCompactCodemode } from "../lib/codemode-renderer.ts";
17
18
  import { getGentleAiRenderState, renderGentleAiLifecycleCall, renderGentleAiResult, type GentleAiRenderContext } from "../lib/gentle-ai-renderer.ts";
18
- import { sanitizeTerminalText } from "../lib/terminal-theme.ts";
19
+ import {
20
+ CARD_TONE, cardAwaitingResult, cardBottom, cardInnerWidth, cardLine, cardRunningLine, cardTopRows, floatRows, markCardResult,
21
+ type CardRowContext, type CardTheme,
22
+ } from "../lib/shell-card.ts";
23
+ import { sanitizeTerminalText, stripAnsi } from "../lib/terminal-theme.ts";
19
24
 
20
25
  type QuietToolName = "read" | "bash" | "grep" | "find" | "ls" | "edit" | "write";
26
+ type RegisteredToolName = Exclude<QuietToolName, "bash">;
21
27
  type ThemeLike = {
22
28
  bold(value: string): string;
23
29
  fg(color: string, value: string): string;
@@ -25,13 +31,23 @@ type ThemeLike = {
25
31
 
26
32
  const TOOL_CREATORS = {
27
33
  read: createReadToolDefinition,
28
- bash: createBashTool,
29
34
  grep: createGrepTool,
30
35
  find: createFindTool,
31
36
  ls: createLsTool,
32
37
  edit: createEditTool,
33
38
  write: createWriteTool,
34
- } satisfies Record<QuietToolName, (cwd: string) => any>;
39
+ } satisfies Record<RegisteredToolName, (cwd: string) => any>;
40
+
41
+ // Caller-owned identities: shared notices and lower widgets retain their glyphs.
42
+ const TOOL_GLYPH = {
43
+ read: "≡",
44
+ bash: "$",
45
+ grep: "⌕",
46
+ find: "⌖",
47
+ ls: "☷",
48
+ edit: "✎",
49
+ write: "+",
50
+ } as const satisfies Record<QuietToolName, string>;
35
51
 
36
52
  const COLLAPSED_COUNT_LABELS: Partial<Record<QuietToolName, string>> = {
37
53
  grep: "matches",
@@ -49,17 +65,17 @@ const EMPTY_RESULT_MESSAGES: Partial<Record<QuietToolName, string[]>> = {
49
65
  bash: ["(no output)"],
50
66
  };
51
67
 
52
- const toolCache = new Map<string, Record<QuietToolName, any>>();
68
+ const toolCache = new Map<string, Record<RegisteredToolName, any>>();
53
69
 
54
- function createBuiltInTools(cwd: string): Record<QuietToolName, any> {
70
+ function createBuiltInTools(cwd: string): Record<RegisteredToolName, any> {
55
71
  return Object.fromEntries(
56
- (Object.entries(TOOL_CREATORS) as [QuietToolName, (cwd: string) => any][]).map(
72
+ (Object.entries(TOOL_CREATORS) as [RegisteredToolName, (cwd: string) => any][]).map(
57
73
  ([name, createTool]) => [name, createTool(cwd)],
58
74
  ),
59
- ) as Record<QuietToolName, any>;
75
+ ) as Record<RegisteredToolName, any>;
60
76
  }
61
77
 
62
- function getBuiltInTools(cwd: string): Record<QuietToolName, any> {
78
+ function getBuiltInTools(cwd: string): Record<RegisteredToolName, any> {
63
79
  let tools = toolCache.get(cwd);
64
80
  if (!tools) {
65
81
  tools = createBuiltInTools(cwd);
@@ -167,7 +183,7 @@ function isGitCommand(args: Record<string, unknown> | undefined): boolean {
167
183
  return /^(?:env\s+\S+=\S+\s+|command\s+|\w+=\S+\s+)*git(?:\s|$)/.test(command);
168
184
  }
169
185
 
170
- export type GentleAiRoutineCommand = "sdd-status" | "sdd-continue" | "sdd-attempt" | "review";
186
+ export type GentleAiRoutineCommand = "review";
171
187
 
172
188
  const GENTLE_AI_EXECUTABLE = String.raw`(?:gentle-ai(?:\.exe)?|(?:\.{1,2}[\\/]|(?:[A-Za-z]:)?(?:[\\/][^\\/\r\n]+)*[\\/])\.gentle-ai[\\/]v\d+\.\d+\.\d+[\\/]gentle-ai(?:\.exe)?)`;
173
189
  const GENTLE_AI_COMMAND_ARGUMENTS = new RegExp(`^${GENTLE_AI_EXECUTABLE}$`);
@@ -238,8 +254,6 @@ function shellTokens(command: string): ShellTokenization {
238
254
  function isAssignment(token: string): boolean {
239
255
  return /^[A-Za-z_][A-Za-z0-9_]*=/.test(token);
240
256
  }
241
- const SDD_ATTEMPT_VERBS = new Set(["grant"]);
242
-
243
257
  const REVIEW_DIRECT_OPERATIONS = new Set([
244
258
  "capabilities",
245
259
  "start",
@@ -304,22 +318,6 @@ function displayToken(token: string): string {
304
318
  return token.replace(/-/g, " ");
305
319
  }
306
320
 
307
- function authorizationRootCount(tokens: string[]): number {
308
- let count = 0;
309
- for (let index = 0; index < tokens.length; index += 1) {
310
- const token = tokens[index]!;
311
- if (token === "--authorization-root") {
312
- const value = tokens[index + 1];
313
- if (value !== undefined && !value.startsWith("-")) count += 1;
314
- continue;
315
- }
316
- if (token.startsWith("--authorization-root=") && token.slice("--authorization-root=".length).length > 0) {
317
- count += 1;
318
- }
319
- }
320
- return count;
321
- }
322
-
323
321
  function validateGate(tokens: string[]): string | undefined {
324
322
  const gateFlag = tokens.findIndex((token) => token === "--gate" || token.startsWith("--gate="));
325
323
  if (gateFlag < 0) return undefined;
@@ -332,7 +330,7 @@ function validateGate(tokens: string[]): string | undefined {
332
330
  /**
333
331
  * Matches only supported routine Gentle AI CLI calls, including bounded
334
332
  * package-local paths, not arbitrary shell output that merely mentions
335
- * gentle-ai. These commands otherwise emit machine-readable SDD/RDD data.
333
+ * gentle-ai. Review commands otherwise emit machine-readable RDD data.
336
334
  */
337
335
  export function isGentleAiDirectCommand(args: Record<string, unknown> | undefined, commandArguments = GENTLE_AI_COMMAND_ARGUMENTS): boolean {
338
336
  return gentleAiCommandTokens(args, commandArguments) !== undefined;
@@ -341,24 +339,12 @@ export function isGentleAiDirectCommand(args: Record<string, unknown> | undefine
341
339
  export function gentleAiRoutineCommand(args: Record<string, unknown> | undefined, commandArguments = GENTLE_AI_COMMAND_ARGUMENTS): GentleAiRoutineCommand | undefined {
342
340
  const tokens = gentleAiCommandTokens(args, commandArguments);
343
341
  if (!tokens) return undefined;
344
- if (tokens[0] === "sdd-status") return "sdd-status";
345
- if (tokens[0] === "sdd-continue") return "sdd-continue";
346
- if (tokens[0] === "sdd-attempt" && SDD_ATTEMPT_VERBS.has(tokens[1] ?? "")) return "sdd-attempt";
347
342
  if (tokens[0] === "review") return "review";
348
343
  return undefined;
349
344
  }
350
345
 
351
- function gentleAiOperationPathFrom(tokens: string[]): string {
352
- if (tokens[0] === "sdd-status") return "sdd status";
353
- if (tokens[0] === "sdd-continue") return "sdd continue";
354
- if (tokens[0] === "sdd-attempt") {
355
- const verb = tokens[1] ?? "";
356
- if (!SDD_ATTEMPT_VERBS.has(verb)) return "sdd attempt";
357
- const rootCount = authorizationRootCount(tokens);
358
- return rootCount > 0
359
- ? `sdd attempt grant · ${rootCount} root${rootCount === 1 ? "" : "s"}`
360
- : "sdd attempt grant";
361
- }
346
+ function gentleAiOperationPathFrom(tokens: string[]): string | undefined {
347
+ if (tokens[0]?.startsWith("sdd-")) return undefined;
362
348
  if (tokens[0] === "version") return "version";
363
349
  if (tokens[0] !== "review") return "command";
364
350
 
@@ -385,11 +371,6 @@ export function gentleAiOperationPath(args: Record<string, unknown> | undefined,
385
371
  return tokens ? gentleAiOperationPathFrom(tokens) : undefined;
386
372
  }
387
373
 
388
- export function isGentleAiGrantCommand(args: Record<string, unknown> | undefined, commandArguments = GENTLE_AI_COMMAND_ARGUMENTS): boolean {
389
- const tokens = gentleAiCommandTokens(args, commandArguments);
390
- return tokens?.[0] === "sdd-attempt" && tokens[1] === "grant";
391
- }
392
-
393
374
  interface ToolResultFormatOptions {
394
375
  expanded: boolean;
395
376
  isError?: boolean;
@@ -501,7 +482,7 @@ function formatToolCall(toolName: QuietToolName, args: Record<string, unknown>,
501
482
  case "bash": {
502
483
  const command = safeText(asString(args.command, "..."));
503
484
  const timeout = typeof args.timeout === "number" ? theme.fg("muted", ` (timeout ${args.timeout}s)`) : "";
504
- return `${theme.fg("toolTitle", theme.bold(`$ ${command}`))}${timeout}`;
485
+ return `${theme.fg("toolTitle", theme.bold(`bash $ ${command}`))}${timeout}`;
505
486
  }
506
487
  case "grep": {
507
488
  let text = `${theme.fg("toolTitle", theme.bold("grep"))} ${theme.fg("accent", `/${safeText(asString(args.pattern))}/`)} in ${safeText(shortenPath(args.path) || ".")}`;
@@ -574,6 +555,78 @@ class BoundedRows implements Component {
574
555
  invalidate(): void {}
575
556
  }
576
557
 
558
+ // Tool card: every quiet tool call is a rounded petal card, like the Gentle AI
559
+ // card, so the call and its output read as one block apart from assistant
560
+ // prose. pi does not paint tool backgrounds for renderShell "self", so the
561
+ // frame tone carries the status the painted Box would otherwise show. The call
562
+ // owns the top rule; the result draws the sides and always closes the frame.
563
+ // Until the first result exists, the call closes the frame itself.
564
+ type ToolTone = typeof CARD_TONE.WARNING | typeof CARD_TONE.SUCCESS | typeof CARD_TONE.ERROR;
565
+
566
+ function toolTone(pending: boolean, failed: boolean): ToolTone {
567
+ if (failed) return CARD_TONE.ERROR;
568
+ return pending ? CARD_TONE.WARNING : CARD_TONE.SUCCESS;
569
+ }
570
+
571
+ class ToolCardTop implements Component {
572
+ private readonly header: () => string;
573
+ private readonly glyph: string;
574
+ private readonly tone: ToolTone;
575
+ private readonly theme: CardTheme;
576
+ private readonly hint: string | undefined;
577
+ private readonly row: CardRowContext | undefined;
578
+
579
+ constructor(header: () => string, tone: ToolTone, theme: CardTheme, glyph: string, hint?: string, row?: CardRowContext) {
580
+ this.header = header;
581
+ this.glyph = glyph;
582
+ this.tone = tone;
583
+ this.theme = theme;
584
+ this.hint = hint;
585
+ this.row = row;
586
+ }
587
+
588
+ /** Renders `╭─ <icon> <call> ── <hint> ╮`, reserving applicable hint space before wrapping the call. A call that does not fit the rule (a long or multi-line command) continues on card rows below it, so every line of the command stays visible. */
589
+ render(width: number): string[] {
590
+ const target = Math.max(0, Math.floor(width));
591
+ if (target === 0) return [];
592
+ const running = this.row !== undefined && cardAwaitingResult(this.row);
593
+ return floatRows(this.tone, this.theme, target, (inner) => ({
594
+ head: cardTopRows({ title: this.header(), glyph: this.glyph, body: [], tone: this.tone }, this.theme, inner, this.hint),
595
+ body: running ? [cardRunningLine(this.tone, this.theme, inner)] : undefined,
596
+ bottom: running ? cardBottom(this.tone, this.theme, inner) : undefined,
597
+ }));
598
+ }
599
+
600
+ invalidate(): void {}
601
+ }
602
+
603
+ class ToolCardBody implements Component {
604
+ private readonly inner: () => Component;
605
+ private readonly tone: ToolTone;
606
+ private readonly theme: CardTheme;
607
+
608
+ constructor(inner: () => Component, tone: ToolTone, theme: CardTheme) {
609
+ this.inner = inner;
610
+ this.tone = tone;
611
+ this.theme = theme;
612
+ }
613
+
614
+ /** Renders the inner component between the card sides and closes the frame, even when the result has no rows. */
615
+ render(width: number): string[] {
616
+ const target = Math.max(0, Math.floor(width));
617
+ if (target === 0) return [];
618
+ return floatRows(this.tone, this.theme, target, (inner) => ({
619
+ body: this.inner().render(cardInnerWidth(inner)).map((line) => cardLine(line.trimEnd(), this.tone, this.theme, inner)),
620
+ bottom: cardBottom(this.tone, this.theme, inner),
621
+ afterHeading: true,
622
+ }));
623
+ }
624
+
625
+ invalidate(): void {
626
+ // Content is rebuilt with the current theme at render time.
627
+ }
628
+ }
629
+
577
630
  function shouldRenderPreviewTail(
578
631
  toolName: QuietToolName,
579
632
  text: string,
@@ -596,13 +649,6 @@ function hasImageContent(result: AgentToolResult<unknown>): boolean {
596
649
  return result.content.some((content) => content.type === "image");
597
650
  }
598
651
 
599
- function hasExpandableContent(toolName: QuietToolName, result: AgentToolResult<unknown>, text: string): boolean {
600
- if (isEmptyResultMessage(toolName, text)) return false;
601
- if (text.length > 0 || hasImageContent(result)) return true;
602
- const diff = detailsRecord(result).diff;
603
- return toolName === "edit" && typeof diff === "string" && diff.length > 0;
604
- }
605
-
606
652
  function sanitizedRenderContext(context: ToolRenderContextLike | undefined): ToolRenderContextLike {
607
653
  if (!context) return { args: {} };
608
654
  return {
@@ -639,17 +685,21 @@ function gentleAiRenderTransition(
639
685
  return { directResult: false };
640
686
  }
641
687
 
642
- function registerQuietTool(pi: ExtensionAPI, toolName: QuietToolName, commandArguments: () => RegExp): void {
643
- const registrationTool = getBuiltInTools(process.cwd())[toolName];
644
- const officialRenderResult = registrationTool.renderResult;
688
+ /** Rendering-only factory; Bash renderers are not attached to production native Bash. */
689
+ export function createQuietToolRenderer(
690
+ toolName: QuietToolName,
691
+ resolveOverride: GentleAiDevBinaryOverrideResolver = () => undefined,
692
+ timing: () => GentleAiElapsedTimingLedger | undefined = () => undefined,
693
+ officialRenderResult?: ToolDefinition["renderResult"],
694
+ ): Pick<ToolDefinition, "renderShell" | "renderCall" | "renderResult"> {
695
+ const commandArguments = () => createGentleAiCommandArguments(resolveQuietToolsDevBinaryPath(resolveOverride));
696
+ const withElapsedTiming = (context: GentleAiRenderContext): GentleAiRenderContext => {
697
+ const ledger = timing();
698
+ return ledger ? { ...context, elapsedTiming: ledger } : context;
699
+ };
645
700
 
646
- pi.registerTool({
647
- ...registrationTool,
701
+ return {
648
702
  renderShell: "self",
649
- async execute(toolCallId, params, signal, onUpdate, ctx) {
650
- const runtimeTool = getBuiltInTools(ctx.cwd)[toolName];
651
- return runtimeTool.execute(toolCallId, params, signal, onUpdate, ctx);
652
- },
653
703
  renderCall(args, theme, context) {
654
704
  const callArgs = args as Record<string, unknown>;
655
705
  const renderContext = sanitizedRenderContext(context as ToolRenderContextLike | undefined);
@@ -658,13 +708,18 @@ function registerQuietTool(pi: ExtensionAPI, toolName: QuietToolName, commandArg
658
708
  : undefined;
659
709
  if (operationPath) {
660
710
  const detail = renderContext.expanded === true && typeof callArgs.command === "string" ? `$ ${callArgs.command}` : undefined;
661
- return renderGentleAiLifecycleCall(operationPath, theme, renderContext as GentleAiRenderContext, detail);
711
+ return renderGentleAiLifecycleCall(operationPath, theme, withElapsedTiming(renderContext as GentleAiRenderContext), detail);
662
712
  }
663
- return new Text(formatToolCall(toolName, callArgs, theme), 0, 0);
713
+ const tone = toolTone(renderContext.isPartial !== false, renderContext.isError === true);
714
+ // Same place and wording as the Gentle AI card: the expand key rides the top rule once the call finished.
715
+ const finished = renderContext.isPartial === false && (renderContext.executionStarted === true || renderContext.isError === true);
716
+ const hint = finished ? stripAnsi(keyHint("app.tools.expand", renderContext.expanded === true ? "to collapse" : "to expand")) : undefined;
717
+ return new ToolCardTop(() => formatToolCall(toolName, callArgs, theme), tone, theme, TOOL_GLYPH[toolName], hint, renderContext);
664
718
  },
665
719
  /** Builds the card component for this render pass; collapsed cards delegate to the wrapped-line cache keyed by the tool result object. */
666
720
  renderResult(result, options, theme, context) {
667
721
  const renderContext = context as ToolRenderContextLike | undefined;
722
+ markCardResult(renderContext?.state);
668
723
  const cacheKey = typeof result === "object" && result !== null ? result : undefined;
669
724
  const safeResult = sanitizedResult(result);
670
725
  const text = safeText(extractTextContent(safeResult));
@@ -676,49 +731,86 @@ function registerQuietTool(pi: ExtensionAPI, toolName: QuietToolName, commandArg
676
731
  { result: true, isPartial: options.isPartial },
677
732
  ).directResult;
678
733
  if (directResult) {
679
- return renderGentleAiResult(safeResult, { expanded: options.expanded, isPartial: options.isPartial, isError }, theme, renderContext as GentleAiRenderContext | undefined);
734
+ return renderGentleAiResult(safeResult, { expanded: options.expanded, isPartial: options.isPartial, isError }, theme, renderContext ? withElapsedTiming(renderContext as GentleAiRenderContext) : undefined);
680
735
  }
736
+ const resultTone = toolTone(options.isPartial === true, isError);
737
+ const carded = (component: () => Component): Component => new ToolCardBody(component, resultTone, theme);
681
738
  if (options.isPartial) {
682
- if (options.expanded) return new Text(`${theme.fg("warning", partialLabel(toolName, text))}\n${theme.fg("muted", text)}`, 0, 0);
739
+ if (options.expanded) return carded(() => new Text(`${theme.fg("warning", partialLabel(toolName, text))}\n${theme.fg("muted", text)}`, 0, 0));
683
740
  const visible = lastOutputLines(text, PREVIEW_LINE_LIMIT);
684
- return new BoundedRows([
741
+ return carded(() => new BoundedRows([
685
742
  { text: theme.fg("warning", partialLabel(toolName, text)), rows: 1 },
686
743
  ...(visible ? [{ text: theme.fg("muted", visible), rows: PREVIEW_LINE_LIMIT, tail: true }] : []),
687
- ], cacheKey);
744
+ ], cacheKey));
688
745
  }
689
746
  if (options.expanded && toolName === "read" && hasImageContent(safeResult) && officialRenderResult) {
690
- return officialRenderResult(
747
+ return carded(() => officialRenderResult(
691
748
  safeResult,
692
749
  options,
693
750
  theme,
694
751
  sanitizedRenderContext(renderContext) as any,
695
- );
752
+ ));
696
753
  }
697
- const output = formatToolResultOutput(toolName, safeResult, {
754
+ let output = formatToolResultOutput(toolName, safeResult, {
698
755
  expanded: options.expanded,
699
756
  isError,
700
757
  args: renderContext?.args,
701
758
  });
702
- const hint = !options.expanded && !directResult && hasExpandableContent(toolName, safeResult, text)
703
- ? `\n${keyHint("app.tools.expand", "to expand")}`
704
- : "";
705
- const color = options.expanded ? "toolOutput" : isError ? "error" : "muted";
706
- if (options.expanded) return new Text(output ? theme.fg(color, output) : "", 0, 0);
759
+ // Counts alone hide the result: retain their useful totals alongside actual rows.
760
+ if (!options.expanded && !isError && output && COLLAPSED_COUNT_LABELS[toolName]) {
761
+ output += `\n${firstLines(text, PREVIEW_LINE_LIMIT - 1)}`;
762
+ }
763
+ if (!options.expanded && !isError && toolName === "edit") {
764
+ const diff = detailsRecord(safeResult).diff;
765
+ if (typeof diff === "string") output += `\n${firstLines(safeText(diff).split("\n").filter((line) => /^[+-]/.test(line)).join("\n"), PREVIEW_LINE_LIMIT - 1)}`;
766
+ }
767
+ const color = isError ? "error" : options.expanded ? "toolOutput" : "muted";
768
+ if (options.expanded) return carded(() => new Text(output ? theme.fg(color, output.replace(/^\n/, "")) : "", 0, 0));
707
769
  if (output) {
708
770
  const tail = shouldRenderPreviewTail(toolName, text, isError, renderContext?.args);
709
- return new BoundedRows([
771
+ return carded(() => new BoundedRows([
710
772
  { text: theme.fg(color, output.replace(/^\n/, "")), rows: PREVIEW_LINE_LIMIT, tail },
711
- ...(hint ? [{ text: theme.fg(color, hint.slice(1)), rows: 1 }] : []),
712
- ], cacheKey);
773
+ ], cacheKey));
713
774
  }
714
- return new Text(hint ? theme.fg(color, hint.slice(1)) : "", 0, 0);
775
+ // The card top rule carries the expand key, so an empty result only closes the frame.
776
+ return carded(() => new Text("", 0, 0));
715
777
  },
778
+ };
779
+ }
780
+
781
+ function registerQuietTool(pi: ExtensionAPI, toolName: RegisteredToolName, resolveOverride: GentleAiDevBinaryOverrideResolver, timing: () => GentleAiElapsedTimingLedger | undefined): void {
782
+ const registrationTool = getBuiltInTools(process.cwd())[toolName];
783
+ pi.registerTool({
784
+ ...registrationTool,
785
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
786
+ return getBuiltInTools(ctx.cwd)[toolName].execute(toolCallId, params, signal, onUpdate, ctx);
787
+ },
788
+ ...createQuietToolRenderer(toolName, resolveOverride, timing, registrationTool.renderResult),
716
789
  });
717
790
  }
718
791
 
719
- export default function quietTools(pi: ExtensionAPI, resolveOverride: GentleAiDevBinaryOverrideResolver = () => resolveGentleAiDevBinaryOverride()): void {
792
+ export default function quietTools(pi: ExtensionAPI, resolveOverride: GentleAiDevBinaryOverrideResolver = () => resolveGentleAiDevBinaryOverride()): ReturnType<ExtensionFactory> {
720
793
  if (!quietToolsEnabled()) return;
721
- for (const toolName of Object.keys(TOOL_CREATORS) as QuietToolName[]) {
722
- registerQuietTool(pi, toolName, () => createGentleAiCommandArguments(resolveQuietToolsDevBinaryPath(resolveOverride)));
794
+ let elapsedTiming: GentleAiElapsedTimingLedger | undefined;
795
+ pi.on("session_start", (_event, ctx) => {
796
+ elapsedTiming = new GentleAiElapsedTimingLedger(ctx.sessionManager, pi);
797
+ });
798
+ // Only bash calls that render as gentle-ai cards carry a durable duration;
799
+ // every other quiet tool keeps its plain renderer and writes no entries.
800
+ const recordGentleTiming = (event: { toolCallId: string; toolName: string; args?: unknown }, endedAt?: number): void => {
801
+ const ledger = elapsedTiming;
802
+ if (!ledger || event.toolName !== "bash" || !isGentleAiDirectCommand(event.args as Record<string, unknown> | undefined, createGentleAiCommandArguments(resolveQuietToolsDevBinaryPath(resolveOverride)))) return;
803
+ try {
804
+ if (endedAt === undefined) ledger.recordStart(event.toolCallId, Date.now());
805
+ else ledger.recordEnd(event.toolCallId, endedAt);
806
+ } catch { /* Timing persistence is best-effort and never breaks the tool event. */ }
807
+ };
808
+ pi.on("tool_execution_start", (event) => recordGentleTiming(event));
809
+ pi.on("tool_execution_end", (event) => recordGentleTiming(event, Date.now()));
810
+ const withElapsedTiming = (context: GentleAiRenderContext): GentleAiRenderContext =>
811
+ elapsedTiming ? { ...context, elapsedTiming } : context;
812
+ for (const toolName of Object.keys(TOOL_CREATORS) as RegisteredToolName[]) {
813
+ registerQuietTool(pi, toolName, resolveOverride, () => elapsedTiming);
723
814
  }
815
+ return registerCompactCodemode(pi);
724
816
  }
@@ -0,0 +1,60 @@
1
+ import { getAgentDir, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { existsSync, writeFileSync } from "node:fs";
3
+ import {
4
+ isResumeHandoffPath,
5
+ RESUME_HANDOFF_ENV,
6
+ resumeHandoffFromSession,
7
+ serializeResumeHandoff,
8
+ } from "../lib/gentle-shell-resume-hint.ts";
9
+
10
+ // Hands the quitting session to bin/gentle-shell.mjs, which prints a
11
+ // gentle-shell resume command below pi's "pi --session <id>" exit hint (see
12
+ // lib/gentle-shell-resume-hint.ts). Inert unless the launcher set the env var.
13
+
14
+ // Process-wide, not per extension instance: /reload re-runs this factory after
15
+ // the env var was already claimed, and must keep using the same handoff path
16
+ // instead of going inert.
17
+ const STATE_KEY = Symbol.for("gentle-pi.resume-hint");
18
+ type StateHolder = { [STATE_KEY]?: string };
19
+
20
+ function claimHandoffPath(env: NodeJS.ProcessEnv): string | undefined {
21
+ const holder = globalThis as StateHolder;
22
+ const path = env[RESUME_HANDOFF_ENV];
23
+ // Claim the handoff for this process only: subagents and tool shells
24
+ // inherit process.env, and their own shutdowns must not overwrite it.
25
+ delete env[RESUME_HANDOFF_ENV];
26
+ if (holder[STATE_KEY] === undefined && path && isResumeHandoffPath(path)) holder[STATE_KEY] = path;
27
+ return holder[STATE_KEY];
28
+ }
29
+
30
+ /** Test-only: forget the claimed handoff path. */
31
+ export function resetResumeHintState(): void {
32
+ delete (globalThis as StateHolder)[STATE_KEY];
33
+ }
34
+
35
+ export default function resumeHint(pi: ExtensionAPI, env: NodeJS.ProcessEnv = process.env): void {
36
+ const handoffPath = claimHandoffPath(env);
37
+ if (!handoffPath) return;
38
+
39
+ pi.on("session_shutdown", (event, ctx) => {
40
+ if (event.reason !== "quit" || ctx.mode !== "tui") return;
41
+ const sessionManager = ctx.sessionManager;
42
+ const handoff = resumeHandoffFromSession({
43
+ sessionId: sessionManager.getSessionId(),
44
+ sessionDir: sessionManager.getSessionDir(),
45
+ sessionFile: sessionManager.getSessionFile(),
46
+ cwd: sessionManager.getCwd(),
47
+ launchCwd: process.cwd(),
48
+ agentDir: getAgentDir(),
49
+ fileExists: existsSync,
50
+ });
51
+ if (!handoff) return;
52
+ try {
53
+ // "wx": create only. The launcher's private dir starts empty, so an
54
+ // existing file (or a planted symlink) means it is not ours to write.
55
+ writeFileSync(handoffPath, serializeResumeHandoff(handoff), { encoding: "utf8", flag: "wx" });
56
+ } catch {
57
+ // Best effort: without a handoff the launcher prints nothing extra.
58
+ }
59
+ });
60
+ }
@@ -19,7 +19,6 @@ const CACHE_REL_PATH = ".atl/.skill-registry.cache.json";
19
19
  const SECTION_MARKER = "## Skills";
20
20
  const EXCLUDE_NAMES = new Set(["_shared", "skill-registry"]);
21
21
  const EXCLUDE_PREFIXES = ["sdd-"];
22
- const ATL_IGNORE_ENTRY = ".atl/";
23
22
  const WATCH_DEBOUNCE_MS = 500;
24
23
  const REGISTRY_SCHEMA_VERSION = 7;
25
24
  const NO_SKILL_REGISTRY_FLAG = "no-skill-registry";
@@ -315,19 +314,23 @@ interface RegenResult {
315
314
  }
316
315
 
317
316
  async function ensureAtlIgnored(cwd: string): Promise<void> {
318
- const gitignorePath = join(cwd, ".gitignore");
319
- let existing = "";
317
+ const atlDir = join(cwd, ".atl");
318
+ const gitignorePath = join(atlDir, ".gitignore");
320
319
  if (await pathExists(gitignorePath)) {
321
- existing = await readFile(gitignorePath, "utf8");
320
+ const existing = await readFile(gitignorePath, "utf8");
321
+ const ignoreRules = existing
322
+ .split("\n")
323
+ .map((line) => line.trim())
324
+ .filter((line) => line !== "" && !line.startsWith("#"));
325
+ if (ignoreRules.at(-1) === "*") {
326
+ return;
327
+ }
328
+ const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
329
+ await writeFile(gitignorePath, `${existing}${prefix}*\n`);
330
+ return;
322
331
  }
323
- const hasAtlIgnore = existing
324
- .split("\n")
325
- .map((line) => line.trim())
326
- .some((line) => line === ".atl" || line === ATL_IGNORE_ENTRY);
327
- if (hasAtlIgnore) return;
328
- const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
329
- const header = existing.includes("# Local Pi runtime state") ? "" : "# Local Pi runtime state\n";
330
- await writeFile(gitignorePath, `${existing}${prefix}${header}${ATL_IGNORE_ENTRY}\n`);
332
+ await mkdir(atlDir, { recursive: true });
333
+ await writeFile(gitignorePath, "*\n");
331
334
  }
332
335
 
333
336
  function isGeneratedLegacyProjectRegistry(source: string): boolean {
@@ -534,6 +537,7 @@ export const __testing = {
534
537
  parseFrontmatter,
535
538
  renderRegistry,
536
539
  regenerateRegistry,
540
+ ensureAtlIgnored,
537
541
  shouldSkipSkillRegistryStartup,
538
542
  shouldSkipDuplicateExtensionLoad,
539
543
  startSkillRegistryWatcher,