gentle-pi 3.3.0 → 3.5.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 (88) hide show
  1. package/README.md +88 -59
  2. package/assets/orchestrator-delegation.md +1 -1
  3. package/bin/gentle-shell.mjs +198 -0
  4. package/docs/assets/brand/gentle-shell-banner.gif +0 -0
  5. package/docs/assets/diagrams/odd-workflow.svg +74 -0
  6. package/docs/assets/features/agents-view.png +0 -0
  7. package/docs/assets/features/changes-view.png +0 -0
  8. package/docs/assets/features/command-palette.png +0 -0
  9. package/docs/assets/features/profiles-routing.png +0 -0
  10. package/docs/gentle-agents-activity.md +95 -0
  11. package/docs/gentle-shell.md +26 -2
  12. package/docs/readme-reference.md +99 -6
  13. package/extensions/ask-user-choice.ts +70 -22
  14. package/extensions/ask-user-question.ts +338 -0
  15. package/extensions/gentle-agents.ts +41 -1
  16. package/extensions/gentle-ai.ts +59 -18
  17. package/extensions/gentle-shell.ts +99 -10
  18. package/extensions/quiet-tools.ts +28 -5
  19. package/extensions/startup-banner.ts +25 -10
  20. package/lib/agents-rpc-publisher.ts +342 -0
  21. package/lib/agents-runner.ts +7 -2
  22. package/lib/animation-policy.ts +52 -0
  23. package/lib/background-cache-warming.ts +38 -0
  24. package/lib/command-palette-catalog.ts +1 -0
  25. package/lib/gentle-shell-launcher.ts +482 -0
  26. package/lib/inprocess-reviewer.ts +38 -1
  27. package/lib/native-review-cli.ts +36 -10
  28. package/lib/questionnaire/questionnaire-view.ts +603 -0
  29. package/lib/questionnaire/schema.ts +82 -0
  30. package/lib/questionnaire/validate.ts +141 -0
  31. package/lib/review-candidate-view-owner.ts +20 -5
  32. package/lib/review-candidate-view.ts +9 -2
  33. package/lib/review-host-relay.ts +10 -0
  34. package/lib/review-integration-v2.ts +4 -1
  35. package/lib/rpc-host.ts +36 -0
  36. package/lib/shell-bar.ts +13 -0
  37. package/lib/shell-sidebar-layout.ts +10 -4
  38. package/lib/shell-usage-view.ts +5 -2
  39. package/lib/shell-usage.ts +120 -6
  40. package/package.json +5 -1
  41. package/runtime/gentle-shell-launcher.mjs +483 -0
  42. package/runtime/native-review-cli.mjs +35 -9
  43. package/runtime/review-integration-v2.mjs +4 -1
  44. package/scripts/build-runtime-modules.mjs +1 -0
  45. package/scripts/gentle-ai-installer.mjs +10 -10
  46. package/scripts/install-gentle-ai.mjs +14 -7
  47. package/scripts/install-tui-mode-setting.mjs +78 -1
  48. package/scripts/verify-package-files.mjs +6 -3
  49. package/tests/agents-rpc-publisher.test.ts +407 -0
  50. package/tests/agents-runner.test.ts +10 -0
  51. package/tests/animation-policy.test.ts +42 -0
  52. package/tests/ask-user-choice.test.ts +129 -0
  53. package/tests/ask-user-question.test.ts +661 -0
  54. package/tests/background-cache-warming.test.ts +60 -0
  55. package/tests/background-subagents.test.ts +68 -0
  56. package/tests/command-palette.test.ts +9 -0
  57. package/tests/gentle-agents.test.ts +161 -2
  58. package/tests/gentle-ai-binary.test.ts +1 -1
  59. package/tests/gentle-ai-installer.test.ts +47 -47
  60. package/tests/gentle-ai.test.ts +56 -4
  61. package/tests/gentle-shell-bin.test.ts +188 -0
  62. package/tests/gentle-shell-launcher.test.ts +718 -0
  63. package/tests/gentle-shell.test.ts +355 -2
  64. package/tests/inprocess-reviewer.test.ts +92 -0
  65. package/tests/install-tui-mode-guard.test.ts +99 -0
  66. package/tests/install-tui-mode-setting.test.ts +39 -1
  67. package/tests/native-review-capability-contract.test.ts +16 -1
  68. package/tests/native-review-parity.test.ts +19 -0
  69. package/tests/package-manifest.test.ts +6 -17
  70. package/tests/questionnaire-schema.test.ts +274 -0
  71. package/tests/questionnaire-view.test.ts +446 -0
  72. package/tests/rdd-status-line.test.ts +21 -4
  73. package/tests/review-candidate-owner-retry.test.ts +63 -0
  74. package/tests/review-candidate-view.test.ts +15 -0
  75. package/tests/review-controller-native-routing.test.ts +86 -0
  76. package/tests/review-host-relay.test.ts +21 -0
  77. package/tests/review-integration-v2.test.ts +30 -0
  78. package/tests/review-ledger-contract.test.ts +1 -2
  79. package/tests/review-relay-transport-agent.test.ts +107 -2
  80. package/tests/review-risk-assessment.test.ts +104 -0
  81. package/tests/rpc-host.test.ts +77 -0
  82. package/tests/shell-bar.test.ts +8 -0
  83. package/tests/shell-sidebar-layout.test.ts +60 -5
  84. package/tests/shell-usage.test.ts +129 -0
  85. package/tests/skill-collision-prefixes.test.ts +1 -1
  86. package/tests/startup-banner.test.ts +93 -2
  87. package/docs/assets/brand/gentle-pi-banner.png +0 -0
  88. package/skills/release/SKILL.md +0 -137
@@ -133,6 +133,47 @@ const options = [
133
133
  { label: "Preserve requested hash", description: "Keep the hash from the original request.", value: "preserve_requested_hash" },
134
134
  ];
135
135
 
136
+ const INTERACTIVE_HOST_ENV = "GENTLE_SHELL_INTERACTIVE_HOST";
137
+
138
+ function withInteractiveHostEnv(t: { after(fn: () => void): void }): void {
139
+ const previous = process.env[INTERACTIVE_HOST_ENV];
140
+ process.env[INTERACTIVE_HOST_ENV] = "1";
141
+ t.after(() => {
142
+ if (previous === undefined) delete process.env[INTERACTIVE_HOST_ENV];
143
+ else process.env[INTERACTIVE_HOST_ENV] = previous;
144
+ });
145
+ }
146
+
147
+ /** Fake interactive-RPC-host ctx: scripted `select`/`input` answers, one per call. */
148
+ function rpcHostContext(selectAnswers: readonly (string | undefined)[], inputAnswers: readonly (string | undefined)[] = []) {
149
+ const selectCalls: Array<{ title: string; options: string[] }> = [];
150
+ const inputCalls: Array<{ title: string; placeholder?: string }> = [];
151
+ let selectIndex = 0;
152
+ let inputIndex = 0;
153
+ return {
154
+ ctx: {
155
+ mode: "rpc",
156
+ hasUI: true,
157
+ ui: {
158
+ select: async (title: string, opts: string[]) => {
159
+ selectCalls.push({ title, options: opts });
160
+ const answer = selectAnswers[selectIndex];
161
+ selectIndex += 1;
162
+ return answer;
163
+ },
164
+ input: async (title: string, placeholder?: string) => {
165
+ inputCalls.push({ title, placeholder });
166
+ const answer = inputAnswers[inputIndex];
167
+ inputIndex += 1;
168
+ return answer;
169
+ },
170
+ },
171
+ },
172
+ selectCalls,
173
+ inputCalls,
174
+ };
175
+ }
176
+
136
177
  function tuiContext(inputs: readonly string[], rendered: { value: string }) {
137
178
  return {
138
179
  mode: "tui",
@@ -483,6 +524,80 @@ test("ask_user_choice cancels without a value and remains unavailable outside th
483
524
  );
484
525
  });
485
526
 
527
+ test("ask_user_choice stays unavailable on a plain rpc host without the interactive-host variable", async (t) => {
528
+ const { tool } = registerChoiceTool();
529
+ t.after(() => { delete process.env[INTERACTIVE_HOST_ENV]; });
530
+ delete process.env[INTERACTIVE_HOST_ENV];
531
+
532
+ await assert.rejects(
533
+ () => tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, { mode: "rpc", hasUI: true, ui: { select: async () => undefined } }),
534
+ /unavailable outside the interactive TUI/,
535
+ );
536
+ });
537
+
538
+ /** Expected `gentle-pi:ask-user-choice:blocked` sequence around one RPC-dialog turn. */
539
+ const RPC_BLOCKED_SEQUENCE = [
540
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: true } },
541
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: false } },
542
+ ];
543
+
544
+ test("ask_user_choice resolves a selection through RPC dialogs on an interactive host", async (t) => {
545
+ withInteractiveHostEnv(t);
546
+ const registration = registerChoiceTool();
547
+ const { tool } = registration;
548
+ const { ctx, selectCalls } = rpcHostContext([options[1]!.label]);
549
+
550
+ const result = await tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, ctx);
551
+
552
+ assert.equal(selectCalls.length, 1);
553
+ assert.equal(selectCalls[0]?.title, "Proceed?");
554
+ assert.deepEqual(selectCalls[0]?.options, [options[0]!.label, options[1]!.label]);
555
+ assert.deepEqual(result.details.selection, { value: options[1]!.value, label: options[1]!.label, index: 2 });
556
+ assert.equal(result.content[0]?.text, `User selected: 2. ${options[1]!.label} (value: ${options[1]!.value})`);
557
+ assert.deepEqual(registration.emittedEvents(), RPC_BLOCKED_SEQUENCE);
558
+ });
559
+
560
+ test("ask_user_choice resolves an Other… custom response through ctx.ui.input on an interactive host", async (t) => {
561
+ withInteractiveHostEnv(t);
562
+ const registration = registerChoiceTool();
563
+ const { tool } = registration;
564
+ const { ctx, selectCalls, inputCalls } = rpcHostContext(["Other…"], ["a free-form reply"]);
565
+
566
+ const result = await tool.execute("call", { question: "Explain?", options, allowCustomResponse: true }, new AbortController().signal, undefined, ctx);
567
+
568
+ assert.deepEqual(selectCalls[0]?.options, [options[0]!.label, options[1]!.label, "Other…"]);
569
+ assert.equal(inputCalls.length, 1);
570
+ assert.equal(inputCalls[0]?.title, "Explain?");
571
+ assert.deepEqual(result.details, { question: "Explain?", options, customResponse: "a free-form reply" });
572
+ assert.equal(result.content[0]?.text, "User responded: a free-form reply");
573
+ assert.deepEqual(registration.emittedEvents(), RPC_BLOCKED_SEQUENCE);
574
+ });
575
+
576
+ test("ask_user_choice cancels through RPC dialogs like the TUI path when select returns undefined", async (t) => {
577
+ withInteractiveHostEnv(t);
578
+ const registration = registerChoiceTool();
579
+ const { tool } = registration;
580
+ const { ctx } = rpcHostContext([undefined]);
581
+
582
+ const result = await tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, ctx);
583
+
584
+ assert.deepEqual(result.details, { question: "Proceed?", options, cancelled: true });
585
+ assert.equal(result.content[0]?.text, "User cancelled the choice");
586
+ assert.deepEqual(registration.emittedEvents(), RPC_BLOCKED_SEQUENCE, "the blocked lifecycle must still balance on a cancel");
587
+ });
588
+
589
+ test("ask_user_choice cancels through RPC dialogs when the Other… input is cancelled", async (t) => {
590
+ withInteractiveHostEnv(t);
591
+ const registration = registerChoiceTool();
592
+ const { tool } = registration;
593
+ const { ctx } = rpcHostContext(["Other…"], [undefined]);
594
+
595
+ const result = await tool.execute("call", { question: "Explain?", options, allowCustomResponse: true }, new AbortController().signal, undefined, ctx);
596
+
597
+ assert.deepEqual(result.details, { question: "Explain?", options, cancelled: true });
598
+ assert.deepEqual(registration.emittedEvents(), RPC_BLOCKED_SEQUENCE, "the blocked lifecycle must still balance on a cancel");
599
+ });
600
+
486
601
  test("ask_user_choice emits a private balanced lifecycle around selection and cancellation", async () => {
487
602
  const sequence: string[] = [];
488
603
  const selectedRegistration = registerChoiceTool([], ({ data }) => {
@@ -582,3 +697,17 @@ test("ask_user_choice is offered only for interactive TUI turns and preserves th
582
697
  for (const hook of registration.hooks) await hook({}, { mode: "print" });
583
698
  assert.deepEqual(registration.activeTools(), ["read", "ask_user_question"]);
584
699
  });
700
+
701
+ test("ask_user_choice is offered on an interactive rpc host and withdrawn once the variable is gone", async (t) => {
702
+ const registration = registerChoiceTool(["read"]);
703
+ registration.allowRuntimeActions();
704
+ t.after(() => { delete process.env[INTERACTIVE_HOST_ENV]; });
705
+
706
+ process.env[INTERACTIVE_HOST_ENV] = "1";
707
+ for (const hook of registration.hooks) await hook({}, { mode: "rpc" });
708
+ assert.deepEqual(registration.activeTools(), ["read", "ask_user_choice"]);
709
+
710
+ delete process.env[INTERACTIVE_HOST_ENV];
711
+ for (const hook of registration.hooks) await hook({}, { mode: "rpc" });
712
+ assert.deepEqual(registration.activeTools(), ["read"]);
713
+ });