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
@@ -6,10 +6,12 @@
6
6
  // process, filesystem, and child process.
7
7
  import {
8
8
  accessSync,
9
+ chmodSync,
9
10
  closeSync,
10
11
  constants as fsConstants,
11
12
  existsSync,
12
13
  mkdirSync,
14
+ mkdtempSync,
13
15
  openSync,
14
16
  readdirSync,
15
17
  readFileSync,
@@ -19,8 +21,7 @@ import {
19
21
  statSync,
20
22
  writeFileSync,
21
23
  } from "node:fs";
22
- import { createRequire } from "node:module";
23
- import { constants as osConstants, homedir } from "node:os";
24
+ import { constants as osConstants, homedir, tmpdir } from "node:os";
24
25
  import { delimiter, dirname, join, resolve as resolvePath } from "node:path";
25
26
  import { spawn, spawnSync } from "node:child_process";
26
27
  import { fileURLToPath } from "node:url";
@@ -53,6 +54,13 @@ import {
53
54
  restoreJsonField,
54
55
  shellQuote,
55
56
  } from "../runtime/gentle-shell-launcher.mjs";
57
+ import {
58
+ parseResumeHandoff,
59
+ planResumeHint,
60
+ RESUME_HANDOFF_DIR_PREFIX,
61
+ RESUME_HANDOFF_ENV,
62
+ RESUME_HANDOFF_FILE,
63
+ } from "../runtime/gentle-shell-resume-hint.mjs";
56
64
  import { GENTLE_AI_VERSION, gentleAiBinaryPath, PackageLocalGentleAiBinaryMissingError } from "../runtime/gentle-ai-binary.mjs";
57
65
  import { DEFAULT_THEME_NAME, installIsolatedTuiModeSetting } from "../scripts/install-tui-mode-setting.mjs";
58
66
 
@@ -72,17 +80,30 @@ function readJsonIfExists(path) {
72
80
  }
73
81
  }
74
82
 
75
- // @earendil-works/pi-coding-agent ships as an optional peer dependency: it may
76
- // not be installed at all, so a resolution failure here is expected, not an error.
83
+ // Resolve the public ESM entry without importing the agent or reaching through
84
+ // its exports map. Only an absent optional peer permits PATH fallback; malformed
85
+ // installed metadata must not silently select a different runtime.
77
86
  function resolveBundledCli() {
87
+ let publicEntry;
78
88
  try {
79
- const require = createRequire(import.meta.url);
80
- const pkgJsonPath = require.resolve("@earendil-works/pi-coding-agent/package.json");
81
- const cliPath = join(dirname(pkgJsonPath), "dist", "bundle", "cli.js");
82
- return existsSync(cliPath) ? cliPath : undefined;
83
- } catch {
84
- return undefined;
89
+ publicEntry = fileURLToPath(import.meta.resolve("@earendil-works/pi-coding-agent"));
90
+ } catch (error) {
91
+ if (error.code === "ERR_MODULE_NOT_FOUND") return undefined;
92
+ throw error;
85
93
  }
94
+ const entry = realpathSync(publicEntry);
95
+ const root = dirname(dirname(entry));
96
+ const expectedEntry = join(root, "dist", "index.js");
97
+ const metadata = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
98
+ if (entry !== expectedEntry || metadata.name !== "@earendil-works/pi-coding-agent" ||
99
+ metadata.bin?.pi !== "dist/bundle/cli.js") {
100
+ throw new Error("Unsupported adjacent Pi package entry/name/bin metadata");
101
+ }
102
+ const cliPath = join(root, metadata.bin.pi);
103
+ if (realpathSync(cliPath) !== cliPath || !statSync(cliPath).isFile()) {
104
+ throw new Error("Unsupported adjacent Pi CLI path");
105
+ }
106
+ return cliPath;
86
107
  }
87
108
 
88
109
  function findOnPath(name) {
@@ -162,6 +183,45 @@ function isDirectory(path) {
162
183
  }
163
184
  }
164
185
 
186
+ // Reuse Herdr's managed bridge, never its transport. This process-boundary
187
+ // lookup is deliberately best-effort and does not modify either agent home.
188
+ function managedHerdrExtensionArgs(home, args) {
189
+ const env = process.env;
190
+ if (home.mode !== "isolated" || args.piSubcommand !== undefined || args.passthrough[0] === "mcp") return [];
191
+ if (env.HERDR_ENV !== "1" || !env.HERDR_SOCKET_PATH?.trim() || !env.HERDR_PANE_ID?.trim()) return [];
192
+ if (env.GENTLE_PI_AGENTS_CHILD === "1" || !process.stdin.isTTY || !process.stdout.isTTY) return [];
193
+ // Only automatic interactive loading: a user opt-out must not become an
194
+ // explicit -e (which Pi loads even under --no-extensions). Conservatively
195
+ // skip ambiguous mode flags too; --mode text alone still allows a TUI.
196
+ const forwarded = args.passthrough;
197
+ if (forwarded.some((arg) => ["--no-extensions", "-ne", "--print", "-p", "--export", "--list-models", "-v"].includes(arg))) return [];
198
+ if (forwarded.some((arg, i) => (arg === "--mode" && forwarded[i + 1] !== "text") || arg.startsWith("--mode="))) return [];
199
+ try {
200
+ if (!statSync(env.HERDR_SOCKET_PATH).isSocket()) return [];
201
+ } catch {
202
+ return [];
203
+ }
204
+ // Prefer a bridge in the selected home over adding a competing copy; keep
205
+ // the incoming Pi home override before falling back to Herdr's usual home.
206
+ const agentHomes = [home.dir, env.PI_CODING_AGENT_DIR, join(homedir(), ".pi", "agent")];
207
+ for (const agentHome of agentHomes) {
208
+ if (!agentHome) continue;
209
+ const bridge = join(agentHome, "extensions", "herdr-agent-state.ts");
210
+ try {
211
+ if (!statSync(bridge).isFile()) continue;
212
+ accessSync(bridge, fsConstants.R_OK);
213
+ // Pi's package-manager.toResolvedPaths and resource-loader.mergePaths
214
+ // dedupe canonical files across discovery, explicit -e and manifests.
215
+ // Existence alone is NOT proof of loading: declare the resource and
216
+ // let that resolver dedupe it, including explicit aliases from argv.
217
+ return ["-e", realpathSync(bridge)];
218
+ } catch {
219
+ // An absent/unreadable bridge never prevents the ordinary launch.
220
+ }
221
+ }
222
+ return [];
223
+ }
224
+
165
225
  // Real-fs adapter for discoverLooseExtensionEntries (lib/gentle-shell-launcher.ts):
166
226
  // statSync-based isFile/isDirectory (not readdirSync's Dirent, which uses
167
227
  // lstat and so would treat a symlinked file or directory as neither) so a
@@ -254,9 +314,46 @@ function readRawConfig(configPath) {
254
314
  function writeRawConfig(configPath, config) {
255
315
  const configDir = dirname(configPath);
256
316
  if (!existsSync(configDir)) mkdirSync(configDir, { recursive: true, mode: 0o700 });
257
- const tempPath = join(configDir, `.${basenameOf(configPath)}.gentle-shell-${process.pid}.tmp`);
258
- writeFileSync(tempPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
259
- renameSync(tempPath, configPath);
317
+ writeFileAtomically(configPath, `${JSON.stringify(config, null, 2)}\n`);
318
+ }
319
+
320
+ // The one atomic write every launcher-owned config/settings update goes
321
+ // through: a temp file next to the real target, then a rename onto it, so a
322
+ // crash or kill never leaves a partial file. When `path` is a symlink (Nix
323
+ // home-manager, stow, and similar dotfile managers ship settings.json and
324
+ // config.json that way), the write lands on the link's real target and the
325
+ // link itself stays in place; renaming onto `path` would replace the link
326
+ // with a regular file. A missing path or a dangling link has no real target
327
+ // to preserve, so it is written at `path` itself, exactly as before this
328
+ // helper existed, instead of creating a file wherever a dangling link points.
329
+ // The permission bits are `mode` when given, otherwise the real target's own
330
+ // (applied with chmod, so the umask cannot narrow them); a new file keeps
331
+ // the default creation mode. The temp file is removed when the write or
332
+ // rename throws, and the error propagates to the caller's own handling.
333
+ function writeFileAtomically(path, data, { mode } = {}) {
334
+ let target = path;
335
+ try {
336
+ target = realpathSync(path);
337
+ } catch (error) {
338
+ if (error.code !== "ENOENT") throw error;
339
+ }
340
+ let targetMode = mode;
341
+ if (targetMode === undefined) {
342
+ try {
343
+ targetMode = statSync(target).mode & 0o777;
344
+ } catch (error) {
345
+ if (error.code !== "ENOENT") throw error;
346
+ }
347
+ }
348
+ const tempPath = join(dirname(target), `.${basenameOf(target)}.gentle-shell-${process.pid}.tmp`);
349
+ try {
350
+ writeFileSync(tempPath, data);
351
+ if (targetMode !== undefined) chmodSync(tempPath, targetMode);
352
+ renameSync(tempPath, target);
353
+ } catch (error) {
354
+ rmSync(tempPath, { force: true });
355
+ throw error;
356
+ }
260
357
  }
261
358
 
262
359
  function loadConfig() {
@@ -512,9 +609,7 @@ function restoreFile(snapshot) {
512
609
  }
513
610
  if (currentBytes !== undefined && currentBytes.equals(snapshot.bytes)) return false;
514
611
  mkdirSync(dirname(path), { recursive: true });
515
- const tempPath = join(dirname(path), `.${basenameOf(path)}.gentle-shell-restore-${process.pid}.tmp`);
516
- writeFileSync(tempPath, snapshot.bytes, { mode: snapshot.mode });
517
- renameSync(tempPath, path);
612
+ writeFileAtomically(path, snapshot.bytes, { mode: snapshot.mode });
518
613
  return true;
519
614
  }
520
615
 
@@ -574,10 +669,7 @@ function restoreManagedAssetDigestField(path, originalText) {
574
669
  if (currentText === undefined) return false;
575
670
  const restoredText = restoreJsonField(originalText, currentText, MANAGED_ASSET_DIGEST_FIELD);
576
671
  if (restoredText === undefined) return false;
577
- const mode = statSync(path).mode & 0o777;
578
- const tempPath = join(dirname(path), `.${basenameOf(path)}.gentle-shell-restore-${process.pid}.tmp`);
579
- writeFileSync(tempPath, restoredText, { mode });
580
- renameSync(tempPath, path);
672
+ writeFileAtomically(path, restoredText);
581
673
  return true;
582
674
  }
583
675
 
@@ -618,10 +710,7 @@ function enforceDefaultThemeField(settingsPath, originalSettingsText) {
618
710
  ? restoreJsonField(originalSettingsText, currentText, "theme")
619
711
  : forceJsonFieldIfAbsentInOriginal(originalSettingsText, currentText, "theme", DEFAULT_THEME_NAME);
620
712
  if (newText === undefined) return false;
621
- const mode = statSync(settingsPath).mode & 0o777;
622
- const tempPath = join(dirname(settingsPath), `.${basenameOf(settingsPath)}.gentle-shell-restore-${process.pid}.tmp`);
623
- writeFileSync(tempPath, newText, { mode });
624
- renameSync(tempPath, settingsPath);
713
+ writeFileAtomically(settingsPath, newText);
625
714
  return originalHadTheme ? "restored" : "forced";
626
715
  }
627
716
 
@@ -641,6 +730,53 @@ function safely(label, path, fallback, fn) {
641
730
  }
642
731
  }
643
732
 
733
+ // gentle-pi replaces Pi's replaceable builtin codemode with its own decorated
734
+ // codemode tool (extensions/quiet-tools.ts -> registerCompactCodemode in
735
+ // lib/codemode-renderer.ts), and Pi prints a startup warning whenever a
736
+ // builtin loses its tool to another extension. Pi's only per-builtin opt-out
737
+ // is a `-builtin:<name>` entry in the settings `extensions` array, so every
738
+ // normal launch ensures that entry in the settings.json of a home
739
+ // gentle-shell owns (see main, below).
740
+ const BUILTIN_CODEMODE_EXTENSION = "builtin:codemode";
741
+
742
+ // Pure: returns `settingsText` with `-<builtin>` appended to its `extensions`
743
+ // array (created when absent), or undefined when nothing should change — the
744
+ // text does not parse as a JSON object, `extensions` exists but is not an
745
+ // array, or the array already holds any explicit entry for the builtin
746
+ // (`+`, `-`, `!`, or bare), which is the user's own decision. Keeps every
747
+ // other key and entry in place, plus the text's own indentation and trailing
748
+ // newline (same detection as detectJsonFormatting in
749
+ // lib/gentle-shell-launcher.ts).
750
+ function withBuiltinExtensionExcluded(settingsText, builtin) {
751
+ let settings;
752
+ try {
753
+ settings = JSON.parse(settingsText);
754
+ } catch {
755
+ return undefined;
756
+ }
757
+ if (typeof settings !== "object" || settings === null || Array.isArray(settings)) return undefined;
758
+ const extensions = Object.prototype.hasOwnProperty.call(settings, "extensions") ? settings.extensions : [];
759
+ if (!Array.isArray(extensions)) return undefined;
760
+ if (extensions.some((entry) => typeof entry === "string" && entry.replace(/^[+!-]/, "") === builtin)) return undefined;
761
+ const indent = settingsText.match(/\{\r?\n([ \t]+)/)?.[1];
762
+ const serialized = JSON.stringify({ ...settings, extensions: [...extensions, `-${builtin}`] }, null, indent);
763
+ return settingsText.endsWith("\n") ? `${serialized}\n` : serialized;
764
+ }
765
+
766
+ // Applies withBuiltinExtensionExcluded to an existing settings.json with the
767
+ // same atomic temp-file-then-rename write as the restores above. A missing
768
+ // settings.json is left missing: only the brand-new-home bootstrap
769
+ // (installIsolatedTuiModeSetting) seeds that file, and this launch-time step
770
+ // never takes over that role. Returns true when it actually wrote the file.
771
+ function ensureBuiltinCodemodeExcluded(settingsPath) {
772
+ const currentText = readJsonIfExists(settingsPath);
773
+ if (currentText === undefined) return false;
774
+ const newText = withBuiltinExtensionExcluded(currentText, BUILTIN_CODEMODE_EXTENSION);
775
+ if (newText === undefined) return false;
776
+ writeFileAtomically(settingsPath, newText);
777
+ return true;
778
+ }
779
+
644
780
  // Provisions `home` with everything `gentle-ai install --agent pi` installs
645
781
  // into a regular Pi, by spawning the package-local pinned gentle-ai binary
646
782
  // (never a PATH `gentle-ai`) with PI_CODING_AGENT_DIR/GENTLE_PI_AGENT_HOME set
@@ -1139,6 +1275,21 @@ async function main() {
1139
1275
  // exits with the same signal-derived code instead of falling through
1140
1276
  // to launch pi.
1141
1277
  if (autoProvisionResult !== undefined) process.exit(autoProvisionResult.exitCode);
1278
+
1279
+ // Runs after auto-provision, so it sees settings.json exactly as that
1280
+ // run left it. Only a home gentle-shell owns, by the same rule
1281
+ // auto-provisioning uses (homeIsForeign): never --link (excluded above),
1282
+ // a foreign --home, or pi's own default agent home, since plain pi may
1283
+ // share those and would lose its builtin codemode. `setup` (including
1284
+ // --dry-run) returned before this point.
1285
+ const settingsPath = join(home.dir, "settings.json");
1286
+ const excluded = safely("exclude Pi's builtin codemode in your Gentle Shell settings", settingsPath, false, () => {
1287
+ const previous = provisionedEntry(readRawConfig(resolveConfigPath()), safeRealpath(home.dir));
1288
+ return !homeIsForeign(home, previous, homeHadContentBeforeBootstrap) && ensureBuiltinCodemodeExcluded(settingsPath);
1289
+ });
1290
+ if (excluded) {
1291
+ process.stderr.write(`gentle-shell: disabled Pi's builtin codemode in ${settingsPath} (Gentle Shell ships its own codemode tool)\n`);
1292
+ }
1142
1293
  }
1143
1294
 
1144
1295
  const packageRootExplicit = args.packageRoot !== undefined;
@@ -1228,22 +1379,96 @@ async function main() {
1228
1379
  takeOver,
1229
1380
  otherPackagePaths,
1230
1381
  looseExtensionEntries,
1231
- passthrough: args.passthrough,
1382
+ passthrough: [...managedHerdrExtensionArgs(home, args), ...args.passthrough],
1232
1383
  piSubcommand: args.piSubcommand,
1233
1384
  baseEnv: process.env,
1385
+ homedir: homedir(),
1234
1386
  });
1235
1387
 
1388
+ // Only an interactive session ends with pi's exit resume hint, which
1389
+ // gentle-shell completes with its own line; a pi subcommand gets no handoff.
1390
+ const resumeHandoff = args.piSubcommand === undefined ? createResumeHandoff() : undefined;
1391
+ const childEnv = resumeHandoff ? { ...invocation.env, [RESUME_HANDOFF_ENV]: resumeHandoff.path } : invocation.env;
1392
+
1236
1393
  const launchPlan = planSpawn({ command: invocation.command, args: invocation.args, platform: process.platform });
1237
- const child = spawn(launchPlan.command, launchPlan.args, { stdio: "inherit", env: invocation.env, shell: launchPlan.shell });
1394
+ let child;
1395
+ try {
1396
+ child = spawn(launchPlan.command, launchPlan.args, { stdio: "inherit", env: childEnv, shell: launchPlan.shell });
1397
+ } catch (error) {
1398
+ resumeHandoff?.dispose();
1399
+ throw error;
1400
+ }
1401
+ // Only SIGHUP means the terminal is gone. pi may survive a forwarded
1402
+ // SIGINT and keep running, so other signals must not silence the hint.
1403
+ let terminalHungUp = false;
1238
1404
  for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
1239
- process.on(signal, () => child.kill(signal));
1405
+ process.on(signal, () => {
1406
+ if (signal === "SIGHUP") terminalHungUp = true;
1407
+ child.kill(signal);
1408
+ });
1240
1409
  }
1241
- child.on("error", (error) => fail(`Could not start pi: ${error.message}`, 1));
1410
+ child.on("error", (error) => {
1411
+ resumeHandoff?.dispose();
1412
+ fail(`Could not start pi: ${error.message}`, 1);
1413
+ });
1242
1414
  child.on("exit", (code, signal) => {
1243
- process.exit(signal ? signalExitCode(signal) : (code ?? 1));
1415
+ const exitCode = signal ? signalExitCode(signal) : (code ?? 1);
1416
+ if (resumeHandoff) {
1417
+ const hint = planResumeHint({
1418
+ handoff: resumeHandoff.read(),
1419
+ homeFlags: homeSelectorFlags(home),
1420
+ stdoutIsTTY: process.stdout.isTTY === true,
1421
+ terminalHungUp,
1422
+ platform: process.platform,
1423
+ color: process.stdout.hasColors?.() === true,
1424
+ });
1425
+ resumeHandoff.dispose();
1426
+ // TTY writes are asynchronous on Windows: exit only once the
1427
+ // hint is flushed, or it can be lost.
1428
+ if (hint) {
1429
+ // A write error (e.g. EIO on a closed terminal) must not turn
1430
+ // pi's exit into a launcher crash.
1431
+ process.stdout.once("error", () => process.exit(exitCode));
1432
+ process.stdout.write(hint, () => process.exit(exitCode));
1433
+ return;
1434
+ }
1435
+ }
1436
+ process.exit(exitCode);
1244
1437
  });
1245
1438
  }
1246
1439
 
1440
+ // Private temp dir for the resume-hint handoff (lib/gentle-shell-resume-hint.ts).
1441
+ // Best effort: if it cannot be created, only pi's own hint is printed.
1442
+ function createResumeHandoff() {
1443
+ let dir;
1444
+ try {
1445
+ dir = mkdtempSync(join(tmpdir(), RESUME_HANDOFF_DIR_PREFIX));
1446
+ } catch {
1447
+ return undefined;
1448
+ }
1449
+ const path = join(dir, RESUME_HANDOFF_FILE);
1450
+ return {
1451
+ path,
1452
+ read: () => {
1453
+ try {
1454
+ const text = readJsonIfExists(path);
1455
+ return text === undefined ? undefined : parseResumeHandoff(text);
1456
+ } catch {
1457
+ return undefined;
1458
+ }
1459
+ },
1460
+ // Never throws: it runs inside the exit handler, where an EPERM on
1461
+ // Windows would otherwise replace pi's exit code with a crash.
1462
+ dispose: () => {
1463
+ try {
1464
+ rmSync(dir, { recursive: true, force: true });
1465
+ } catch {
1466
+ // A leftover empty temp dir is harmless.
1467
+ }
1468
+ },
1469
+ };
1470
+ }
1471
+
1247
1472
  main().catch((error) => {
1248
1473
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
1249
1474
  process.exit(1);
@@ -6,7 +6,30 @@ How the Gentle Pi orchestrator decides who verifies a bounded writer's work. The
6
6
 
7
7
  The bounded writer runs the exact commands the parent lists under `## Verification`, in the foreground, and reports each as `<command>: <observed result>`. That report is the verification of record and the native review is the independent check. `gentle-ai-verify` is on-demand: a `partial` or `blocked` writer, an expensive or external check the parent wants on a cheaper profile, or a parent spot check.
8
8
 
9
- This `on` path holds only while the native review actually reaches a terminal outcome for the current candidate (gentle-pi#668). A human decline of the consent envelope for this candidate (candidate-scoped, never the RDD kill switch), a clone-local RDD disable discovered mid-flow, or a refused START/STATUS all mean the review never ran, so the parent falls back to the exact risk-gated path below, as if RDD were `off` -- declining a review never lowers the bar below the RDD-off path. `gentle_review`'s `assess` operation accepts an optional `nativeReviewOutcome` (`closed`, `declined`, `unavailable`, or `unknown`) so the caller can state this directly. `closed` is never auto-derived: only a caller that itself just acknowledged the approved review for this exact candidate may pass it, right after that acknowledgement. When `nativeReviewOutcome` is omitted, `assess` only ever tries to auto-derive `declined`/`unavailable`, and only for the exact candidate the event was bound to -- keyed by that candidate's own target identity, never by repository alone, so one candidate's recorded outcome can never leak into a different candidate's `assess` call in the same clone. A missing or mismatched identity fails closed to `unknown`, verified exactly like `off`. The result's `outcome_source` (`explicit`, `derived`, or `unknown`) states which of these produced the value, so a stale or missing derivation is visible rather than silently indistinguishable from a real `unknown`.
9
+ This `on` path holds only while the native review actually reaches a terminal outcome for the current candidate (gentle-pi#668). A human decline of the consent envelope for this candidate (candidate-scoped, never the RDD kill switch), a clone-local RDD disable discovered mid-flow, or a refused START/STATUS all mean the review never ran, so the parent falls back to the exact risk-gated path below, as if RDD were `off` -- declining a review never lowers the bar below the RDD-off path.
10
+
11
+ ### How `assess` resolves the review outcome (gentle-pi#1175)
12
+
13
+ `gentle_review`'s `assess` operation derives `closed` only from the native `candidate.consumed` fact for this exact candidate. Native writes that fact only when the approved review for that candidate is acknowledged. A caller-supplied `closed` is not authority: without that fact it resolves to `unknown`. A declined, unavailable, or unknown outcome falls back to the RDD-off tier table, and `unknown` is never treated as closed.
14
+
15
+ Rows apply in order; the first match wins.
16
+
17
+ | Outcome evidence | Resolved `nativeReviewOutcome` | `outcome_source` |
18
+ | --- | --- | --- |
19
+ | Caller passes `declined`, `unavailable`, or `unknown` | that value | `explicit` |
20
+ | This process recorded `declined`/`unavailable` for this exact candidate | that value (wins over closure) | `derived` |
21
+ | Native reports `candidate.consumed: true` for this candidate | `closed` | `derived` |
22
+ | Anything else, including a bare caller `closed` | `unknown` | `unknown` |
23
+
24
+ A recorded outcome is keyed by the candidate's own target identity, never by repository alone, so one candidate's outcome never leaks into a different candidate's `assess` call in the same clone.
25
+
26
+ ### How `assess` resolves the writer profile (gentle-pi#1175)
27
+
28
+ The writer profile comes from the runtime-recorded model and effort of the pending mutations for the root (`writerProfileSource: runtime`). Caller `writerModelId`/`writerEffort` are only a fallback when no runtime evidence exists (`caller`); with neither, the profile is small (`fallback`). A missing model, a `mini` model token (`gemini` is not mini), or `low` effort keeps the conservative small-model bias.
29
+
30
+ ### What `assess` projects
31
+
32
+ When native reports them, `assess` projects `reviewDue`, `reviewDueReason`, `candidate.consumed`, and the native preflight continuation (`nextTransition`) verbatim. Older binaries omit them, and nothing is invented. The continuation is opaque: relay it unchanged, never rebuild it.
10
33
 
11
34
  ## Receipt-driven development off or unknown (gentle-pi#662)
12
35
 
@@ -23,3 +46,5 @@ The `gentle_review` tool's `assess` operation (`extensions/gentle-ai.ts`) combin
23
46
 
24
47
  When RDD is `on` and the native review closed for this candidate, the writer's own self-verification is the record and the closed native review is the independent check, except a passive-risk change, which still gets a structural readback instead; any other `nativeReviewOutcome` under `on` follows this same tier table instead (gentle-pi#668). The small-model bias raises the medium tier to high for verification purposes only; an unknown RDD line never lowers a tier below `off`. The parent's own spot check (re-running one reported command before delivery) stays required in every tier.
25
48
 
49
+ A native code review is not a substitute for applicable functional checks: tests, builds, and functional verification such as browser checks for UI changes still run when applicable. Review outcomes never authorize delivery; human consent for native review and the user-owned RDD switch are unchanged.
50
+
@@ -90,6 +90,30 @@ When the whole-payload bound is still exceeded after the field- and item-level t
90
90
 
91
91
  An active task's `summary` (running, waiting or queued) is never dropped; only its `thread.items` shrink. Finished tasks can be dropped whole by step 3, oldest first.
92
92
 
93
+ ## Generation and watchdog progress
94
+
95
+ While an active assistant message streams a tool-call block, validated fresh,
96
+ nonempty argument deltas renew the runner's idle watchdog independently of
97
+ thread/display events. `summary.lastStep` becomes `generating tool arguments`
98
+ and `lastActivityAt` advances; no partial argument data is stored in the thread,
99
+ diagnostic, or progress tracker. `toolCalls` increments only at execution start.
100
+ Token/cost totals still update only from finalized assistant `message_end` usage,
101
+ not streaming usage; static totals do not establish inactivity.
102
+
103
+ The tracker admits blocks announced by current RPC identity fields or older Pi
104
+ partial snapshots. It rejects empty/malformed deltas, unannounced or closed
105
+ blocks, stale message starts, and duplicate argument fingerprints. RPC provides
106
+ no delta sequence number, so identical chunks within one block are conservatively
107
+ indistinguishable from replay and do not renew liveness. Only hashes are retained,
108
+ with a 4096-fingerprint bound per assistant message; exhaustion fails closed until
109
+ a newer message starts. Unrelated UI and unrecognized event traffic do not renew
110
+ argument liveness. Existing RPC command-response handling is unchanged.
111
+
112
+ Idle and in-flight execution watchdog budgets are **renewable silence bounds**,
113
+ not absolute run/generation duration limits. Argument generation uses the idle
114
+ budget, not the longer announced-execution budget. Later silence still times out;
115
+ execution start/end and cancellation retain their existing behavior.
116
+
93
117
  ## Coalescing
94
118
 
95
119
  `createRpcActivityPublisher` subscribes to `TaskStore#subscribeSummary` (task added, removed, or changed status) and to `TaskStore#subscribe(id)` for every known task, including ones added after `start()`. Changes inside a 150 ms window collapse into exactly one `setWidget("gentle-agents", [line])` call; `stop()` tears down every subscription and publishes one final frame.