gentle-pi 4.1.0 → 4.3.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 (249) hide show
  1. package/README.md +352 -128
  2. package/assets/agents/gentle-ai-explore.md +1 -0
  3. package/assets/agents/gentle-ai-verify.md +6 -4
  4. package/assets/agents/gentle-ai-worker.md +7 -1
  5. package/assets/orchestrator-delegation-reference.md +163 -0
  6. package/assets/orchestrator-delegation.md +25 -176
  7. package/assets/orchestrator-tracking.md +1 -1
  8. package/assets/orchestrator-verification.md +14 -8
  9. package/assets/orchestrator-writer.md +5 -1
  10. package/assets/orchestrator.md +9 -9
  11. package/bin/engram-guard +1 -1
  12. package/bin/gentle-shell.mjs +188 -35
  13. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  14. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/README.md +7 -0
  15. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/manifest.json +11 -2
  16. package/contracts/review-provider-contract-mirror/v1.4.0/bundle/routing/pi.md +92 -0
  17. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-capabilities.baseline.json +8 -1
  18. package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/generated/provider-roles.baseline.json +1 -1
  19. package/docs/assets/diagrams/agent-orchestration.svg +54 -15
  20. package/docs/assets/diagrams/gentleman-workflow.svg +69 -11
  21. package/docs/assets/diagrams/measured-routing.svg +49 -0
  22. package/docs/assets/diagrams/native-review.svg +52 -12
  23. package/docs/assets/diagrams/odd-workflow.svg +128 -66
  24. package/docs/assets/diagrams/permission-modes.svg +51 -0
  25. package/docs/assets/features/audio-notifications.png +0 -0
  26. package/docs/assets/features/command-palette.png +0 -0
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +40 -8
  30. package/docs/delegated-verification.md +10 -8
  31. package/docs/gentle-agents-activity.md +11 -5
  32. package/docs/gentle-shell-uninstall.md +8 -1
  33. package/docs/gentle-shell.md +20 -12
  34. package/docs/install-wizard.md +17 -10
  35. package/docs/permission-modes.md +51 -6
  36. package/docs/readme-reference.md +155 -31
  37. package/docs/session-profile-format.md +39 -0
  38. package/docs/yolo-mode.md +21 -7
  39. package/extensions/ask-user-choice.ts +4 -0
  40. package/extensions/ask-user-question.ts +6 -1
  41. package/extensions/codegraph-tools.ts +68 -6
  42. package/extensions/gentle-agents.ts +146 -28
  43. package/extensions/gentle-ai.ts +380 -68
  44. package/extensions/gentle-herdr-activity.ts +2 -3
  45. package/extensions/gentle-shell.ts +53 -43
  46. package/extensions/gentle-todo.ts +62 -18
  47. package/extensions/quiet-tools.ts +1 -1
  48. package/extensions/session-scratch.ts +365 -0
  49. package/extensions/startup-banner.ts +2 -3
  50. package/lib/agent-home.ts +8 -3
  51. package/lib/agents-protocol.ts +3 -0
  52. package/lib/agents-runner.ts +55 -6
  53. package/lib/agents-view.ts +3 -1
  54. package/lib/auto-permission-answers.ts +133 -0
  55. package/lib/auto-permission-classifier.ts +215 -36
  56. package/lib/auto-permission-evidence.ts +88 -0
  57. package/lib/auto-permission-memory.ts +69 -0
  58. package/lib/auto-permission-runtime.ts +251 -35
  59. package/lib/auto-permission-tool-context.ts +61 -0
  60. package/lib/bounded-writer-admission.ts +51 -0
  61. package/lib/command-palette-catalog.ts +0 -4
  62. package/lib/commit-slice-signal.ts +100 -0
  63. package/lib/context-backstop.ts +84 -0
  64. package/lib/delivered-test-lint.ts +80 -0
  65. package/lib/destructive-command-guard.ts +215 -28
  66. package/lib/gauge-theme.ts +146 -0
  67. package/lib/gentle-ai-binary.ts +28 -7
  68. package/lib/gentle-shell-bundled.ts +62 -8
  69. package/lib/gentle-shell-import-pi.ts +233 -0
  70. package/lib/gentle-shell-launcher.ts +146 -12
  71. package/lib/gentle-shell-uninstall.ts +18 -1
  72. package/lib/herdr-activity.ts +24 -0
  73. package/lib/model-routing-authority.ts +6 -1
  74. package/lib/native-review-cli.ts +9 -1
  75. package/lib/orchestrator-discovery.ts +5 -1
  76. package/lib/orchestrator-presence.ts +9 -0
  77. package/lib/orchestrator-scope.ts +21 -3
  78. package/lib/provider-contract-bundle.ts +99 -53
  79. package/lib/review-candidate-view-owner.ts +29 -19
  80. package/lib/review-candidate-view.ts +36 -2
  81. package/lib/review-risk-assessment.ts +43 -38
  82. package/lib/review-session-standing-permission.ts +3 -1
  83. package/lib/selection-engine.ts +4 -1
  84. package/lib/session-profile-freeze.ts +40 -16
  85. package/lib/session-profile-persistence.ts +187 -0
  86. package/lib/session-scratch.ts +474 -0
  87. package/lib/shell-bar.ts +25 -25
  88. package/lib/shell-changes-view.ts +80 -13
  89. package/lib/shell-changes.ts +2 -0
  90. package/lib/shell-gauge.ts +49 -6
  91. package/lib/shell-prompt.ts +6 -4
  92. package/lib/shell-todo.ts +39 -8
  93. package/lib/shell-usage-view.ts +5 -1
  94. package/lib/shell-usage.ts +7 -7
  95. package/lib/verifier-admission.ts +29 -0
  96. package/lib/vim-editor-adapter.ts +22 -4
  97. package/lib/yolo-temp-scratch.ts +69 -0
  98. package/package.json +5 -6
  99. package/runtime/gentle-ai-binary.mjs +28 -7
  100. package/runtime/gentle-shell-bundled.mjs +61 -7
  101. package/runtime/gentle-shell-import-pi.mjs +234 -0
  102. package/runtime/gentle-shell-launcher.mjs +144 -10
  103. package/runtime/gentle-shell-uninstall.mjs +18 -1
  104. package/runtime/native-review-cli.mjs +8 -0
  105. package/runtime/review-risk-assessment.mjs +43 -38
  106. package/scripts/build-runtime-modules.mjs +1 -0
  107. package/scripts/bundled-engram-migration.mjs +125 -0
  108. package/scripts/gentle-ai-installer.mjs +10 -10
  109. package/scripts/install-wizard-preview.mjs +2 -2
  110. package/scripts/installer-downloads.mjs +7 -7
  111. package/scripts/installer-preflight.mjs +1 -1
  112. package/scripts/installer-server.mjs +14 -4
  113. package/scripts/main-channel.mjs +3 -2
  114. package/scripts/pi-update.mjs +223 -0
  115. package/scripts/run-test-suite.mjs +5 -0
  116. package/scripts/verify-package-files.mjs +19 -13
  117. package/skills/gentle-ai/SKILL.md +3 -3
  118. package/skills/work-unit-commits/SKILL.md +5 -3
  119. package/tests/agent-risk-escalation.test.ts +37 -10
  120. package/tests/agents-runner.test.ts +260 -3
  121. package/tests/append-system-prompt-route.test.ts +18 -1
  122. package/tests/auto-mode-human-answers.test.ts +68 -0
  123. package/tests/auto-mode-invariants.test.ts +23 -5
  124. package/tests/auto-mode-sdk.test.ts +29 -2
  125. package/tests/auto-permission-answers.test.ts +170 -0
  126. package/tests/auto-permission-classifier.test.ts +353 -2
  127. package/tests/auto-permission-compound.test.ts +24 -0
  128. package/tests/auto-permission-evidence.test.ts +88 -0
  129. package/tests/auto-permission-memory.test.ts +46 -0
  130. package/tests/auto-permission-public-regressions.test.ts +111 -0
  131. package/tests/auto-permission-runtime.test.ts +270 -12
  132. package/tests/autonomous-guard.test.ts +208 -16
  133. package/tests/bounded-writer-admission.test.ts +11 -3
  134. package/tests/bundled-install.test.ts +3 -3
  135. package/tests/card-content-policy.test.ts +19 -9
  136. package/tests/child-safety.test.ts +39 -0
  137. package/tests/codegraph-tools.test.ts +123 -2
  138. package/tests/command-palette.test.ts +14 -4
  139. package/tests/commit-slice-signal.test.ts +162 -0
  140. package/tests/context-backstop.test.ts +159 -0
  141. package/tests/delegate-for-reason-contract.test.ts +91 -24
  142. package/tests/delivered-test-lint.test.ts +105 -0
  143. package/tests/destructive-command-guard.test.ts +85 -1
  144. package/tests/engram-safety.test.ts +35 -7
  145. package/tests/fixtures/primary-session-prompt/gentleman.golden.md +143 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.4.0/README.md +29 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.4.0/manifest.json +83 -0
  148. package/tests/fixtures/provider-contract-bundle/v1.4.0/orchestration/pi.md +70 -0
  149. package/tests/fixtures/provider-contract-bundle/v1.4.0/routing/pi.md +92 -0
  150. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/lens.schema.json +16 -0
  151. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/refuter.schema.json +1 -0
  152. package/tests/fixtures/provider-contract-bundle/v1.4.0/schemas/targeted-validator.schema.json +1 -0
  153. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/lens.json +1 -0
  154. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/refuter.json +1 -0
  155. package/tests/fixtures/provider-contract-bundle/v1.4.0/vectors/targeted-validator.json +1 -0
  156. package/tests/gauge-theme.test.ts +120 -0
  157. package/tests/gentle-agents.test.ts +443 -8
  158. package/tests/gentle-ai-binary.test.ts +1 -1
  159. package/tests/gentle-ai-child-guards.test.ts +61 -0
  160. package/tests/gentle-ai-doctor-herdr.test.ts +94 -0
  161. package/tests/gentle-ai-installer.test.ts +51 -51
  162. package/tests/gentle-shell-bin.test.ts +180 -17
  163. package/tests/gentle-shell-bundled.test.ts +235 -38
  164. package/tests/gentle-shell-import-pi.test.ts +262 -0
  165. package/tests/gentle-shell-launcher.test.ts +97 -13
  166. package/tests/gentle-shell-uninstall.test.ts +30 -0
  167. package/tests/gentle-shell.test.ts +113 -14
  168. package/tests/gentle-todo.test.ts +258 -27
  169. package/tests/headless-delivery-question.test.ts +58 -0
  170. package/tests/herdr-activity.test.ts +34 -1
  171. package/tests/install-wizard.test.ts +2 -2
  172. package/tests/installer-posix-bootstrap.test.ts +8 -8
  173. package/tests/installer-preflight.test.ts +3 -3
  174. package/tests/installer-probes.test.ts +8 -8
  175. package/tests/installer-runner.test.ts +14 -13
  176. package/tests/installer-server.test.ts +13 -5
  177. package/tests/installer-windows-bootstrap.test.ts +33 -4
  178. package/tests/main-channel-upgrade.test.ts +7 -1
  179. package/tests/native-review-capability-contract.test.ts +7 -1
  180. package/tests/native-review-parity.test.ts +22 -1
  181. package/tests/no-self-pause-contract.test.ts +5 -2
  182. package/tests/odd-integration.test.ts +5 -1
  183. package/tests/odd-routing-canonical-ratchet.test.ts +127 -162
  184. package/tests/odd-routing-contract.test.ts +115 -30
  185. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  186. package/tests/orchestrator-budget.test.ts +19 -8
  187. package/tests/orchestrator-discovery.test.ts +29 -0
  188. package/tests/orchestrator-rdd-ownership.test.ts +3 -1
  189. package/tests/orchestrator-scope.test.ts +66 -0
  190. package/tests/orchestrator-status-publication-contract.test.ts +12 -8
  191. package/tests/package-manifest.test.ts +28 -24
  192. package/tests/pi-update-launcher.test.ts +106 -0
  193. package/tests/pi-update.test.ts +186 -0
  194. package/tests/primary-session-prompt.test.ts +101 -0
  195. package/tests/provider-contract-bundle.test.ts +118 -0
  196. package/tests/provider-contract-mirror.test.ts +24 -0
  197. package/tests/quiet-tool-rendering.test.ts +23 -0
  198. package/tests/rdd-aware-verification-contract.test.ts +29 -11
  199. package/tests/rdd-status-line.test.ts +131 -0
  200. package/tests/review-agent-end-preflight.test.ts +6 -0
  201. package/tests/review-candidate-view.test.ts +49 -0
  202. package/tests/review-contract-prompt.test.ts +5 -2
  203. package/tests/review-controller-native-routing.test.ts +14 -1
  204. package/tests/review-controller.test.ts +1 -1
  205. package/tests/review-host-relay-routing.test.ts +59 -2
  206. package/tests/review-loadout-rdd-off-sdk.test.ts +237 -0
  207. package/tests/review-owner-identity.test.ts +132 -0
  208. package/tests/review-risk-assessment.test.ts +163 -76
  209. package/tests/review-session-standing-permission-controller.test.ts +14 -1
  210. package/tests/review-session-standing-permission.test.ts +27 -1
  211. package/tests/run-test-suite.test.ts +15 -0
  212. package/tests/runtime-harness.mjs +16 -6
  213. package/tests/selection-engine.test.ts +35 -5
  214. package/tests/session-changes-shell.test.ts +1 -1
  215. package/tests/session-profile-freeze.test.ts +37 -1
  216. package/tests/session-profile-persistence.test.ts +334 -0
  217. package/tests/session-scratch.test.ts +945 -0
  218. package/tests/session-subject-routing-contract.test.ts +2 -1
  219. package/tests/shell-bar.test.ts +7 -0
  220. package/tests/shell-changes-view.test.ts +137 -1
  221. package/tests/shell-changes.test.ts +18 -0
  222. package/tests/shell-gauge.test.ts +42 -0
  223. package/tests/shell-todo.test.ts +15 -12
  224. package/tests/small-task-inline-contract.test.ts +37 -0
  225. package/tests/standalone-config-home.test.ts +324 -0
  226. package/tests/startup-banner.test.ts +5 -4
  227. package/tests/startup-home-repo.test.ts +34 -0
  228. package/tests/support/orchestrator-modules.ts +1 -0
  229. package/tests/task-size-routing-contract.test.ts +69 -9
  230. package/tests/test-scratch-contract.test.ts +102 -0
  231. package/tests/test-strength-contract.test.ts +36 -1
  232. package/tests/tool-batching-contract.test.ts +64 -0
  233. package/tests/verification-proportionality.test.ts +46 -0
  234. package/tests/verifier-admission.test.ts +137 -0
  235. package/tests/verify-agent-contract.test.ts +8 -8
  236. package/tests/vim-editor-adapter.test.ts +41 -15
  237. package/tests/windows-hidden-processes.test.ts +5 -0
  238. package/tests/writer-edit-surface-scope.test.ts +3 -0
  239. package/tests/writer-feature-document-admission.test.ts +156 -0
  240. package/tests/yolo-mode.test.ts +24 -1
  241. package/tests/yolo-temp-scratch.test.ts +71 -0
  242. package/scripts/mirror-odd-routing.mjs +0 -242
  243. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/orchestration/pi.md +0 -0
  244. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/lens.schema.json +0 -0
  245. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/refuter.schema.json +0 -0
  246. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  247. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/lens.json +0 -0
  248. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/refuter.json +0 -0
  249. /package/contracts/review-provider-contract-mirror/{v1.3.0 → v1.4.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -80,10 +80,10 @@ function standaloneLauncher(t: test.TestContext) {
80
80
 
81
81
  test("a genuinely absent adjacent peer falls back to PATH", (t) => {
82
82
  const f = standaloneLauncher(t);
83
- writePathPiScript(f.root, "0.99.2");
83
+ writePathPiScript(f.root, "1.2.1");
84
84
  const result = spawnSync(process.execPath, [f.launcher, "--version"], { env: f.env, encoding: "utf8" });
85
85
  assert.equal(result.status, 0, result.stderr);
86
- assert.match(result.stdout, /pi 0\.99\.2/);
86
+ assert.match(result.stdout, /pi 1\.2\.1/);
87
87
  });
88
88
 
89
89
  test("malformed adjacent metadata never silently falls back to PATH; env override still wins", (t) => {
@@ -91,8 +91,8 @@ test("malformed adjacent metadata never silently falls back to PATH; env overrid
91
91
  const peer = join(f.root, "node_modules", "@earendil-works", "pi-coding-agent");
92
92
  mkdirSync(join(peer, "dist", "bundle"), { recursive: true });
93
93
  writeFileSync(join(peer, "dist", "index.js"), "");
94
- writePiScript(join(peer, "dist", "bundle", "cli.js"), "0.99.1");
95
- const fallback = writePathPiScript(f.root, "0.99.2");
94
+ writePiScript(join(peer, "dist", "bundle", "cli.js"), "1.2.0");
95
+ const fallback = writePathPiScript(f.root, "1.2.1");
96
96
  const base = { name: "@earendil-works/pi-coding-agent", type: "module", exports: { ".": { import: "./dist/index.js" } }, bin: { pi: "dist/bundle/cli.js" } };
97
97
  for (const metadata of [{ ...base, name: "impostor" }, { ...base, bin: { pi: "../outside.js" } }, { ...base, bin: {} }, "malformed"]) {
98
98
  writeFileSync(join(peer, "package.json"), metadata === "malformed" ? "{" : JSON.stringify(metadata));
@@ -103,7 +103,7 @@ test("malformed adjacent metadata never silently falls back to PATH; env overrid
103
103
  writeFileSync(join(peer, "package.json"), JSON.stringify({ ...base, name: "impostor" }));
104
104
  const override = spawnSync(process.execPath, [f.launcher, "--version"], { env: { ...f.env, GENTLE_SHELL_PI: fallback }, encoding: "utf8" });
105
105
  assert.equal(override.status, 0, override.stderr);
106
- assert.match(override.stdout, /pi 0\.99\.2/);
106
+ assert.match(override.stdout, /pi 1\.2\.1/);
107
107
  });
108
108
 
109
109
  // The launcher's own gentle-pi version, exactly as `ownPackageVersion()` in
@@ -127,7 +127,7 @@ function fixture(t: test.TestContext) {
127
127
  const home = join(root, "home");
128
128
  mkdirSync(home, { recursive: true });
129
129
  const piScript = join(root, "fake-pi.mjs");
130
- writePiScript(piScript, "0.99.1");
130
+ writePiScript(piScript, "1.2.0");
131
131
  const gentleShellHome = join(root, "gentle-shell-home");
132
132
  const env: NodeJS.ProcessEnv = {
133
133
  ...process.env,
@@ -496,7 +496,7 @@ function writeGentleAiScriptDeclaringGentlePi(path: string, extraSources: string
496
496
  // the invocation), this models what a real `pi remove` does to settings.json
497
497
  // closely enough to prove a later plain launch sees the declaration gone and
498
498
  // resumes the launcher's own injection.
499
- function writePiScriptEditingSettingsOnRemove(path: string, version = "0.99.1") {
499
+ function writePiScriptEditingSettingsOnRemove(path: string, version = "1.2.0") {
500
500
  writeFileSync(
501
501
  path,
502
502
  [
@@ -661,6 +661,123 @@ test("first isolated run bootstraps the home, writes fullscreen, and prints the
661
661
  assert.doesNotMatch(second.stderr, /using a separate home at/);
662
662
  });
663
663
 
664
+ // --- home import-pi ----------------------------------------------------------
665
+
666
+ function importPiFixture(t: test.TestContext) {
667
+ const f = fixture(t);
668
+ const env = { ...f.env, PI_CODING_AGENT_DIR: undefined };
669
+ const piHome = join(f.home, ".pi", "agent");
670
+ mkdirSync(piHome, { recursive: true });
671
+ writeFileSync(join(piHome, "auth.json"), JSON.stringify({ anthropic: { type: "oauth", refresh: "dummy-refresh" } }), { mode: 0o600 });
672
+ writeFileSync(join(piHome, "mcp.json"), JSON.stringify({ mcpServers: {} }));
673
+ writeFileSync(join(piHome, "settings.json"), JSON.stringify({ defaultProvider: "anthropic", defaultModel: "m", packages: ["npm:foreign"], extensions: ["./x.ts"] }));
674
+ writeFileSync(join(piHome, "mcp-cache.json"), "{}");
675
+ return { ...f, env, piHome };
676
+ }
677
+
678
+ test("first isolated run without a pi home keeps the plain --link hint", (t) => {
679
+ const f = fixture(t);
680
+ const first = run({ ...f.env, PI_CODING_AGENT_DIR: undefined }, []);
681
+ assert.equal(first.status, 0, first.stderr);
682
+ assert.match(first.stderr, /Run 'gentle-shell --link' to reuse your pi sign-ins and chats\.\n/);
683
+ assert.doesNotMatch(first.stderr, /import-pi/);
684
+ });
685
+
686
+ test("first isolated run with a pi sign-in also mentions home import-pi", (t) => {
687
+ const f = importPiFixture(t);
688
+ const first = run(f.env, []);
689
+ assert.equal(first.status, 0, first.stderr);
690
+ assert.match(first.stderr, /'gentle-shell --link' to reuse your pi sign-ins and chats, or 'gentle-shell home import-pi'/);
691
+ });
692
+
693
+ test("--help lists home import-pi", (t) => {
694
+ const f = fixture(t);
695
+ const result = run(f.env, ["--help"]);
696
+ assert.match(result.stdout, /gentle-shell \[home selectors\] home import-pi \[--dry-run\]/);
697
+ });
698
+
699
+ test("home import-pi copies sign-ins, MCP servers and default model into the isolated home only", (t) => {
700
+ const f = importPiFixture(t);
701
+ const result = run(f.env, ["home", "import-pi"]);
702
+ assert.equal(result.status, 0, result.stderr);
703
+ assert.match(result.stdout, /auth\.json: copied \(0600\)/);
704
+ assert.match(result.stdout, /settings\.json defaultProvider: copied/);
705
+ assert.doesNotMatch(result.stdout + result.stderr, /dummy-refresh/);
706
+ assert.ok(existsSync(join(f.gentleShellHome, "auth.json")));
707
+ assert.equal(existsSync(join(f.gentleShellHome, "mcp-cache.json")), false);
708
+ if (process.platform !== "win32") assert.equal(statSync(join(f.gentleShellHome, "auth.json")).mode & 0o777, 0o600);
709
+ const settings = JSON.parse(readFileSync(join(f.gentleShellHome, "settings.json"), "utf8"));
710
+ assert.equal(settings.defaultProvider, "anthropic");
711
+ assert.equal(settings.tuiMode, "fullscreen", "the home is bootstrapped as a normal first launch would");
712
+ assert.equal(settings.packages, undefined);
713
+ assert.equal(settings.extensions, undefined);
714
+ assert.ok(existsSync(join(f.gentleShellHome, ".gentle-shell-home")));
715
+
716
+ const again = run(f.env, ["home", "import-pi"]);
717
+ assert.equal(again.status, 0, again.stderr);
718
+ assert.match(again.stdout, /auth\.json: already present, kept/);
719
+ assert.match(again.stdout, /Nothing to import/);
720
+ });
721
+
722
+ test("home import-pi --dry-run prints the plan and creates nothing", (t) => {
723
+ const f = importPiFixture(t);
724
+ const result = run(f.env, ["home", "import-pi", "--dry-run"]);
725
+ assert.equal(result.status, 0, result.stderr);
726
+ assert.match(result.stdout, /auth\.json: would copy/);
727
+ assert.equal(existsSync(f.gentleShellHome), false);
728
+ assert.equal(existsSync(join(f.home, ".gentle-shell")), false);
729
+ });
730
+
731
+ test("home import-pi refuses link mode and a target that is the pi home itself", (t) => {
732
+ const f = importPiFixture(t);
733
+ const before = readFileSync(join(f.piHome, "settings.json"), "utf8");
734
+ for (const argv of [["--link", "home", "import-pi"], ["--home", f.piHome, "home", "import-pi"]]) {
735
+ const result = run(f.env, argv);
736
+ assert.equal(result.status, 1, argv.join(" "));
737
+ assert.match(result.stderr, /gentle-shell home import-pi: .*nothing to import/);
738
+ }
739
+ assert.equal(run(f.env, ["home", "link"]).status, 0);
740
+ assert.equal(run(f.env, ["home", "import-pi"]).status, 1);
741
+ assert.equal(readFileSync(join(f.piHome, "settings.json"), "utf8"), before);
742
+ assert.equal(run(f.env, ["home", "import-pi", "--force"]).status, 2);
743
+ });
744
+
745
+ test("a --home filled by home import-pi is still auto-provisioned as gentle-shell's own", (t) => {
746
+ const f = importPiFixture(t);
747
+ const fresh = join(f.root, "fresh-home");
748
+ const empty = join(f.root, "empty-home");
749
+ mkdirSync(empty);
750
+ const gentleAiScript = join(f.root, "fake-gentle-ai.mjs");
751
+ const counterPath = join(f.root, "gentle-ai-runs.log");
752
+ writeGentleAiScriptCountingRuns(gentleAiScript, counterPath);
753
+ const env = enableAutoProvision({ ...f.env, GENTLE_SHELL_GENTLE_AI_BIN: gentleAiScript, GENTLE_SHELL_GENTLE_AI_PIN: "3.6.0" });
754
+ for (const [index, target] of [fresh, empty].entries()) {
755
+ const imported = run(f.env, ["--home", target, "home", "import-pi"]);
756
+ assert.equal(imported.status, 0, imported.stderr);
757
+ assert.match(imported.stdout, /auth\.json: copied/);
758
+ const launch = run(env, ["--home", target, "--mode", "rpc"]);
759
+ assert.equal(launch.status, 0, launch.stderr);
760
+ assert.doesNotMatch(launch.stderr, /already has content and was not set up by gentle-shell/);
761
+ assert.equal(readFileSync(counterPath, "utf8").trim().split("\n").length, index + 1, `auto-provisioning ran for ${target}`);
762
+ }
763
+ });
764
+
765
+ test("home import-pi exits 1 and names the item when a write fails", { skip: process.platform === "win32" }, (t) => {
766
+ const f = importPiFixture(t);
767
+ mkdirSync(f.gentleShellHome, { recursive: true });
768
+ writeFileSync(join(f.gentleShellHome, "settings.json"), "{}");
769
+ writeFileSync(join(f.gentleShellHome, ".gentle-shell-home"), "{}");
770
+ chmodSync(f.gentleShellHome, 0o500);
771
+ let result;
772
+ try {
773
+ result = run(f.env, ["home", "import-pi"]);
774
+ } finally {
775
+ chmodSync(f.gentleShellHome, 0o700); // before the fixture's own cleanup
776
+ }
777
+ assert.equal(result.status, 1);
778
+ assert.match(result.stdout, /auth\.json: failed/);
779
+ });
780
+
664
781
  test("forwarded args reach pi after the injected extension flags, in order", (t) => {
665
782
  const f = fixture(t);
666
783
  const result = run(f.env, ["--mode", "rpc", "-p", "hi"]);
@@ -815,13 +932,24 @@ test("gentle-shell install npm:<pkg> forwards the subcommand and its argument ve
815
932
  assert.equal(payload.PI_CODING_AGENT_DIR, f.gentleShellHome);
816
933
  });
817
934
 
935
+ // #2140: pi dispatches `mcp` on argv[0] only, so an injected -e ahead of it
936
+ // turned `gentle-shell mcp list` into an interactive prompt.
937
+ test("gentle-shell mcp add <server> -- <cmd> forwards to pi's own mcp verbatim, with no injected extension flags", (t) => {
938
+ const f = fixture(t);
939
+ const result = run(f.env, ["mcp", "add", "demo", "--", "npx", "-y", "demo-server"]);
940
+ assert.equal(result.status, 0, result.stderr);
941
+ const payload = JSON.parse(result.stdout);
942
+ assert.deepEqual(payload.args, ["mcp", "add", "demo", "--", "npx", "-y", "demo-server"]);
943
+ assert.equal(payload.PI_CODING_AGENT_DIR, f.gentleShellHome);
944
+ });
945
+
818
946
  test("a too-old pi exits 1 naming both versions", (t) => {
819
947
  const f = fixture(t);
820
948
  writePiScript(f.piScript, "0.80.0");
821
949
  const result = run(f.env, []);
822
950
  assert.equal(result.status, 1);
823
951
  assert.match(result.stderr, /0\.80\.0/);
824
- assert.match(result.stderr, /0\.99\.1/);
952
+ assert.match(result.stderr, /1\.2\.0/);
825
953
  });
826
954
 
827
955
  test("--version prints three lines", (t) => {
@@ -831,7 +959,7 @@ test("--version prints three lines", (t) => {
831
959
  const lines = result.stdout.trim().split("\n");
832
960
  assert.equal(lines.length, 3);
833
961
  assert.match(lines[0], /^gentle-shell /);
834
- assert.match(lines[1], /^pi 0\.99\.1$/);
962
+ assert.match(lines[1], /^pi 1\.2\.0$/);
835
963
  assert.match(lines[2], /^home isolated /);
836
964
  });
837
965
 
@@ -1278,7 +1406,7 @@ test("gentle-shell setup propagates a non-zero pi remove exit code with an actio
1278
1406
  const gentleAiScript = join(f.root, "fake-gentle-ai.mjs");
1279
1407
  writeGentleAiScriptDeclaringConflict(gentleAiScript);
1280
1408
  const failingPiScript = join(f.root, "fake-pi-remove-fails.mjs");
1281
- writePiScript(failingPiScript, "0.99.1", 7);
1409
+ writePiScript(failingPiScript, "1.2.0", 7);
1282
1410
  const env = { ...f.env, GENTLE_SHELL_GENTLE_AI_BIN: gentleAiScript, GENTLE_SHELL_PI: failingPiScript };
1283
1411
 
1284
1412
  const result = run(env, ["setup"]);
@@ -1299,7 +1427,7 @@ test("gentle-shell setup --home <dir> includes --home <dir> in the failing-remov
1299
1427
  const gentleAiScript = join(f.root, "fake-gentle-ai.mjs");
1300
1428
  writeGentleAiScriptDeclaringConflict(gentleAiScript);
1301
1429
  const failingPiScript = join(f.root, "fake-pi-remove-fails.mjs");
1302
- writePiScript(failingPiScript, "0.99.1", 7);
1430
+ writePiScript(failingPiScript, "1.2.0", 7);
1303
1431
  const env = { ...f.env, GENTLE_SHELL_GENTLE_AI_BIN: gentleAiScript, GENTLE_SHELL_PI: failingPiScript };
1304
1432
 
1305
1433
  const result = run(env, ["--home", target, "setup"]);
@@ -1323,7 +1451,7 @@ test("gentle-shell setup shell-quotes a --home path containing a space in the fa
1323
1451
  const gentleAiScript = join(f.root, "fake-gentle-ai.mjs");
1324
1452
  writeGentleAiScriptDeclaringConflict(gentleAiScript);
1325
1453
  const failingPiScript = join(f.root, "fake-pi-remove-fails.mjs");
1326
- writePiScript(failingPiScript, "0.99.1", 7);
1454
+ writePiScript(failingPiScript, "1.2.0", 7);
1327
1455
  const env = { ...f.env, GENTLE_SHELL_GENTLE_AI_BIN: gentleAiScript, GENTLE_SHELL_PI: failingPiScript };
1328
1456
 
1329
1457
  const result = run(env, ["--home", target, "setup"]);
@@ -1865,7 +1993,7 @@ test("setup gives Gentle AI a `pi` on its PATH that runs the same Pi gentle-shel
1865
1993
 
1866
1994
  const result = run(env, ["setup"]);
1867
1995
  assert.equal(result.status, 0, result.stderr);
1868
- assert.deepEqual(JSON.parse(readFileSync(probe, "utf8")), { status: 0, error: null, stdout: "0.99.1\n" });
1996
+ assert.deepEqual(JSON.parse(readFileSync(probe, "utf8")), { status: 0, error: null, stdout: "1.2.0\n" });
1869
1997
  // The launcher is temporary: nothing is left behind in the temp folder it used.
1870
1998
  assert.deepEqual(readdirSync(temp), []);
1871
1999
  });
@@ -2171,7 +2299,7 @@ test("a hung 'pi remove' during post-install cleanup is killed after the timeout
2171
2299
  [
2172
2300
  "#!/usr/bin/env node",
2173
2301
  "const args = process.argv.slice(2);",
2174
- 'if (args.includes("--version")) { console.log("0.99.1"); process.exit(0); }',
2302
+ 'if (args.includes("--version")) { console.log("1.2.0"); process.exit(0); }',
2175
2303
  "if (args[0] === 'remove') { await new Promise((resolve) => setTimeout(resolve, 60000)); process.exit(0); }",
2176
2304
  "console.log(JSON.stringify({ args, PI_CODING_AGENT_DIR: process.env.PI_CODING_AGENT_DIR }));",
2177
2305
  "process.exit(0);",
@@ -2603,6 +2731,13 @@ test("--help mentions the setup subcommand", (t) => {
2603
2731
  assert.match(result.stdout, /\bsetup\b/);
2604
2732
  });
2605
2733
 
2734
+ test("--help lists the mcp subcommand", (t) => {
2735
+ const f = fixture(t);
2736
+ const result = run(f.env, ["--help"]);
2737
+ assert.equal(result.status, 0, result.stderr);
2738
+ assert.match(result.stdout, /gentle-shell mcp /);
2739
+ });
2740
+
2606
2741
  // --- --link takeover of a conflicting path package -----------------------
2607
2742
  //
2608
2743
  // Regression coverage for the settings.json shape that triggered the tool
@@ -3186,7 +3321,7 @@ function writeHandoffPiScript(path: string, exitCode = 0) {
3186
3321
  "#!/usr/bin/env node",
3187
3322
  "const { writeFileSync } = require('node:fs');",
3188
3323
  "const args = process.argv.slice(2);",
3189
- "if (args.includes('--version')) { console.log('0.99.1'); process.exit(0); }",
3324
+ "if (args.includes('--version')) { console.log('1.2.0'); process.exit(0); }",
3190
3325
  "const handoff = process.env.GENTLE_SHELL_RESUME_HANDOFF;",
3191
3326
  "if (handoff) writeFileSync(handoff, JSON.stringify({ sessionId: 'abc' }));",
3192
3327
  "if (process.env.PI_STUB_PRINT_ENV) console.log(JSON.stringify({ args, handoff }));",
@@ -3287,7 +3422,7 @@ function writeWaitingPiScript(path: string) {
3287
3422
  [
3288
3423
  "#!/usr/bin/env node",
3289
3424
  "const { writeFileSync } = require('node:fs');",
3290
- "if (process.argv.includes('--version')) { console.log('0.99.1'); process.exit(0); }",
3425
+ "if (process.argv.includes('--version')) { console.log('1.2.0'); process.exit(0); }",
3291
3426
  "writeFileSync(process.env.GENTLE_SHELL_RESUME_HANDOFF, JSON.stringify({ sessionId: 'abc' }));",
3292
3427
  "const quit = () => { process.stdout.write('To resume this session: pi --session abc\\n'); process.exit(0); };",
3293
3428
  "process.on('SIGHUP', quit);",
@@ -3329,7 +3464,7 @@ test("on a TTY a cross-project session resumes by its session file", { skip: !ha
3329
3464
  [
3330
3465
  "#!/usr/bin/env node",
3331
3466
  "const { writeFileSync } = require('node:fs');",
3332
- "if (process.argv.includes('--version')) { console.log('0.99.1'); process.exit(0); }",
3467
+ "if (process.argv.includes('--version')) { console.log('1.2.0'); process.exit(0); }",
3333
3468
  `writeFileSync(process.env.GENTLE_SHELL_RESUME_HANDOFF, JSON.stringify({ sessionId: 'abc', sessionFile: ${JSON.stringify(sessionFile)} }));`,
3334
3469
  "process.stdout.write('To resume this session: pi --session abc\\n');",
3335
3470
  "",
@@ -3350,3 +3485,31 @@ test("on a TTY without colors the gentle-shell line has no ANSI styling", { skip
3350
3485
  const out = runInPty({ ...f.env, GENTLE_SHELL_PI: piScript, PTY_NO_COLOR: "1" }, ["--link"]);
3351
3486
  assert.ok(out.endsWith(`${PI_HINT}To resume in gentle-shell: gentle-shell --link --session abc\r\n`), JSON.stringify(out));
3352
3487
  });
3488
+
3489
+ // #2143: a Gentle AI that honors GENTLE_AI_PI_EXCLUDE_PACKAGES (gentle-ai#5503)
3490
+ // never declares npm:gentle-pi, so setup neither runs `pi remove` nor prints
3491
+ // the contradictory "removing npm:gentle-pi" line.
3492
+ test("gentle-shell setup asks Gentle AI to skip npm:gentle-pi, so nothing is installed only to be removed", (t) => {
3493
+ const f = fixture(t);
3494
+ const gentleAiScript = join(f.root, "fake-gentle-ai.mjs");
3495
+ writeFileSync(gentleAiScript, [
3496
+ "#!/usr/bin/env node",
3497
+ "import { readFileSync, writeFileSync } from 'node:fs';",
3498
+ "import { join } from 'node:path';",
3499
+ "const excluded = (process.env.GENTLE_AI_PI_EXCLUDE_PACKAGES ?? '').split(',').map((entry) => entry.trim());",
3500
+ "const settingsPath = join(process.env.PI_CODING_AGENT_DIR, 'settings.json');",
3501
+ "const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));",
3502
+ "settings.packages = ['npm:gentle-pi', 'npm:pi-btw'].filter((source) => !excluded.includes(source));",
3503
+ "writeFileSync(settingsPath, JSON.stringify(settings));",
3504
+ "process.stdout.write(JSON.stringify({ args: process.argv.slice(2), excluded: process.env.GENTLE_AI_PI_EXCLUDE_PACKAGES ?? null }) + '\\n');",
3505
+ "",
3506
+ ].join("\n"));
3507
+ chmodSync(gentleAiScript, 0o755);
3508
+ const result = run({ ...f.env, GENTLE_SHELL_GENTLE_AI_BIN: gentleAiScript }, ["setup"]);
3509
+ assert.equal(result.status, 0, result.stderr);
3510
+ assert.doesNotMatch(result.stderr, /removing npm:gentle-pi/);
3511
+ const lines = result.stdout.trim().split("\n").filter((line) => line.length > 0);
3512
+ assert.equal(lines.length, 1, "no pi remove ran: " + result.stdout);
3513
+ assert.deepEqual(JSON.parse(lines[0]), { args: ["install", "--agent", "pi", "--scope", "global"], excluded: "npm:gentle-pi" });
3514
+ });
3515
+