gentle-pi 2.1.2 → 2.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 (228) hide show
  1. package/README.md +108 -79
  2. package/assets/agents/gentle-ai-worker.md +7 -3
  3. package/assets/agents/jd-fix-agent.md +1 -1
  4. package/assets/agents/jd-judge-a.md +3 -1
  5. package/assets/agents/jd-judge-b.md +3 -1
  6. package/assets/agents/review-readability.md +4 -3
  7. package/assets/agents/review-reliability.md +4 -3
  8. package/assets/agents/review-resilience.md +4 -3
  9. package/assets/agents/review-risk.md +4 -3
  10. package/assets/agents/sdd-apply.md +11 -4
  11. package/assets/agents/sdd-archive.md +6 -1
  12. package/assets/agents/sdd-design.md +6 -1
  13. package/assets/agents/sdd-explore.md +6 -2
  14. package/assets/agents/sdd-init.md +10 -2
  15. package/assets/agents/sdd-onboard.md +6 -1
  16. package/assets/agents/sdd-proposal.md +8 -1
  17. package/assets/agents/sdd-research.md +54 -0
  18. package/assets/agents/sdd-spec.md +6 -1
  19. package/assets/agents/sdd-status.md +10 -5
  20. package/assets/agents/sdd-sync.md +6 -1
  21. package/assets/agents/sdd-tasks.md +8 -4
  22. package/assets/agents/sdd-verify.md +27 -2
  23. package/assets/chains/4r-review.chain.md +2 -0
  24. package/assets/chains/sdd-full.chain.md +2 -2
  25. package/assets/chains/sdd-plan.chain.md +1 -1
  26. package/assets/chains/sdd-verify.chain.md +2 -2
  27. package/assets/orchestrator-delegation.md +145 -160
  28. package/assets/orchestrator-memory.md +2 -0
  29. package/assets/orchestrator.md +25 -48
  30. package/assets/sdd-orchestrator-workflow.md +163 -25
  31. package/assets/support/sdd-status-contract.md +24 -6
  32. package/contracts/review-integration/v1/fixtures/consent.fixture.json +3 -3
  33. package/contracts/review-integration/v1/fixtures/start-v2.fixture.json +19 -28
  34. package/contracts/review-integration/v1/fixtures/start.fixture.json +1 -10
  35. package/contracts/review-integration/v1/fixtures/status-v2.fixture.json +12 -21
  36. package/contracts/review-integration/v1/schemas/correction-plan-request.schema.json +49 -0
  37. package/contracts/review-integration/v1/schemas/operation.schema.json +76 -0
  38. package/contracts/review-integration/v1/schemas/repair.schema.json +39 -0
  39. package/contracts/review-integration/v1/schemas/status-v2.schema.json +4 -2
  40. package/contracts/review-integration/v1/schemas/status.schema.json +4 -2
  41. package/contracts/review-integration/v1/schemas/transition-execution.schema.json +42 -0
  42. package/contracts/review-integration/v2/fixtures/consent.fixture.json +1 -1
  43. package/contracts/review-integration/v2/fixtures/start.fixture.json +1 -10
  44. package/contracts/review-integration/v2/fixtures/status.fixture.json +1 -10
  45. package/contracts/review-integration/v2/schemas/failure.schema.json +5 -1
  46. package/contracts/review-integration/v2/schemas/last-event-closure.schema.json +66 -0
  47. package/contracts/review-integration/v2/schemas/opencode-provider-role.schema.json +14 -0
  48. package/contracts/review-integration/v2/schemas/operation.schema.json +6 -1
  49. package/contracts/review-integration/v2/schemas/repair.schema.json +4 -2
  50. package/contracts/review-integration/v2/schemas/start.schema.json +5 -2
  51. package/contracts/review-integration/v2/schemas/status.schema.json +4 -2
  52. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +30 -0
  53. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/README.md +12 -0
  54. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/manifest.json +65 -0
  55. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/lens.schema.json +16 -0
  56. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/refuter.schema.json +1 -0
  57. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/schemas/targeted-validator.schema.json +1 -0
  58. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/lens.json +1 -0
  59. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/refuter.json +1 -0
  60. package/contracts/review-provider-contract-mirror/v1.1.0/bundle/vectors/targeted-validator.json +1 -0
  61. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-capabilities.baseline.json +15 -0
  62. package/contracts/review-provider-contract-mirror/v1.1.0/generated/provider-roles.baseline.json +42 -0
  63. package/docs/native-authority-architecture.md +14 -16
  64. package/docs/review-integration.md +27 -353
  65. package/extensions/ask-user-choice.ts +151 -0
  66. package/extensions/gentle-ai.ts +2710 -3211
  67. package/extensions/quiet-tools.ts +515 -32
  68. package/extensions/sdd-init.ts +21 -12
  69. package/extensions/skill-registry.ts +10 -2
  70. package/extensions/startup-banner.ts +10 -4
  71. package/lib/gentle-ai-binary.ts +173 -2
  72. package/lib/gentle-ai-renderer.ts +70 -0
  73. package/lib/model-routing-authority.ts +133 -0
  74. package/lib/native-review-cli.ts +676 -781
  75. package/lib/opaque-pi-reviewer-adapter.ts +284 -0
  76. package/lib/provider-contract-bundle.ts +704 -0
  77. package/lib/review-candidate-view.ts +836 -118
  78. package/lib/review-compact-contract.ts +59 -248
  79. package/lib/review-host-relay.ts +578 -0
  80. package/lib/review-integration-v2.ts +1254 -221
  81. package/lib/review-last-event-controller.ts +35 -0
  82. package/lib/review-relay-contract.ts +16 -0
  83. package/lib/sdd-preflight.ts +230 -67
  84. package/lib/sdd-status.ts +66 -111
  85. package/lib/terminal-theme.ts +1 -1
  86. package/package.json +83 -79
  87. package/runtime/gentle-ai-binary.mjs +174 -3
  88. package/runtime/native-review-cli.mjs +610 -715
  89. package/runtime/review-integration-v2.mjs +1223 -190
  90. package/runtime/review-relay-contract.mjs +17 -0
  91. package/scripts/{build-git-commit-transaction-runner.mjs → build-runtime-modules.mjs} +5 -5
  92. package/scripts/check-provider-contract.mjs +138 -0
  93. package/scripts/gentle-ai-installer.mjs +85 -22
  94. package/scripts/maintainer/provider-relay-matrix.mjs +403 -0
  95. package/scripts/mirror-provider-contract.mjs +143 -0
  96. package/scripts/test-packed-runner.mjs +20 -9
  97. package/scripts/verify-package-files.mjs +119 -43
  98. package/skills/_shared/review-ledger-contract.md +11 -19
  99. package/skills/chained-pr/SKILL.md +3 -0
  100. package/skills/cognitive-doc-design/SKILL.md +1 -1
  101. package/skills/comment-writer/SKILL.md +1 -1
  102. package/skills/gentle-ai/SKILL.md +7 -74
  103. package/skills/issue-creation/SKILL.md +94 -168
  104. package/skills/judgment-day/SKILL.md +9 -5
  105. package/skills/judgment-day/references/prompts-and-formats.md +2 -0
  106. package/skills/rdd-defect-workflow/SKILL.md +54 -0
  107. package/skills/release/SKILL.md +3 -3
  108. package/skills/skill-registry/SKILL.md +1 -1
  109. package/skills/work-unit-commits/SKILL.md +3 -1
  110. package/tests/artifact-language.test.ts +24 -11
  111. package/tests/ask-user-choice.test.ts +264 -0
  112. package/tests/background-subagents.test.ts +771 -0
  113. package/tests/codegraph-tools.test.ts +3 -3
  114. package/tests/crosslane/cross-lane.mjs +16 -0
  115. package/tests/delegated-key-learnings-contract.test.ts +240 -0
  116. package/tests/devbinary/native-review-parity.devtest.ts +167 -250
  117. package/tests/devbinary/pi-host-relay.devtest.ts +867 -0
  118. package/tests/fixtures/devbinary/capabilities-v2.1.derived.json +331 -0
  119. package/tests/fixtures/devbinary/capabilities-v2.2.captured.json +340 -0
  120. package/tests/fixtures/devbinary/consent-v3.captured.json +37 -0
  121. package/tests/fixtures/devbinary/failure-v2-capture-evidence.captured.json +16 -0
  122. package/tests/fixtures/devbinary/last-event-capture-correction-plan.captured.json +10 -0
  123. package/tests/fixtures/devbinary/last-event-capture-refuter-approved.captured.json +20 -0
  124. package/tests/fixtures/devbinary/last-event-capture-refuter-correction-required.captured.json +26 -0
  125. package/tests/fixtures/devbinary/last-event-capture-result-approved.captured.json +8 -0
  126. package/tests/fixtures/devbinary/last-event-capture-result-correction-required.captured.json +26 -0
  127. package/tests/fixtures/devbinary/last-event-capture-validation-approved.captured.json +8 -0
  128. package/tests/fixtures/devbinary/last-event-closure.provenance.md +13 -0
  129. package/tests/fixtures/devbinary/result-artifact-v2-path.captured.json +12 -0
  130. package/tests/fixtures/devbinary/result-artifact-v2.captured.json +12 -0
  131. package/tests/fixtures/devbinary/review-acknowledged-v1.captured.json +9 -0
  132. package/tests/fixtures/devbinary/review-acknowledged.provenance.md +31 -0
  133. package/tests/fixtures/devbinary/start-v3-consent-declined.captured.json +19 -0
  134. package/tests/fixtures/devbinary/start-v3-consent-granted.captured.json +109 -0
  135. package/tests/fixtures/devbinary/start-v3-zero-lens-closed.captured.json +21 -0
  136. package/tests/fixtures/devbinary/status-v5-capture-result-submission.captured.json +184 -0
  137. package/tests/fixtures/devbinary/status-v5-repository-context.captured.json +138 -0
  138. package/tests/fixtures/devbinary/status-v5.captured.json +88 -0
  139. package/tests/fixtures/native-review-cli/v2.5.0-rc.1/PROVENANCE.txt +15 -0
  140. package/tests/fixtures/provider-contract-bundle/v1.1.0/README.md +12 -0
  141. package/tests/fixtures/provider-contract-bundle/v1.1.0/manifest.json +65 -0
  142. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/lens.schema.json +16 -0
  143. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/refuter.schema.json +1 -0
  144. package/tests/fixtures/provider-contract-bundle/v1.1.0/schemas/targeted-validator.schema.json +1 -0
  145. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/lens.json +1 -0
  146. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/refuter.json +1 -0
  147. package/tests/fixtures/provider-contract-bundle/v1.1.0/vectors/targeted-validator.json +1 -0
  148. package/tests/gentle-ai-binary.test.ts +83 -13
  149. package/tests/gentle-ai-dev-binary-surfacing.test.ts +195 -0
  150. package/tests/gentle-ai-dev-binary.test.ts +159 -0
  151. package/tests/gentle-ai-installer.test.ts +138 -48
  152. package/tests/gentle-ai.test.ts +820 -131
  153. package/tests/gentle-theme.test.ts +133 -0
  154. package/tests/maintainer/provider-relay.maintest.ts +601 -0
  155. package/tests/model-routing-authority.test.ts +257 -0
  156. package/tests/native-review-capability-contract.test.ts +75 -2
  157. package/tests/native-review-cli.test.ts +588 -908
  158. package/tests/native-review-consent.test.ts +283 -38
  159. package/tests/native-review-parity-runtime.test.ts +108 -350
  160. package/tests/native-review-parity.test.ts +684 -732
  161. package/tests/native-sdd-attempt-authority.test.ts +235 -0
  162. package/tests/opaque-pi-reviewer-adapter.test.ts +266 -0
  163. package/tests/orchestrator-budget.test.ts +158 -89
  164. package/tests/orchestrator-rdd-ownership.test.ts +103 -0
  165. package/tests/package-manifest.test.ts +128 -100
  166. package/tests/provider-contract-bundle.test.ts +385 -0
  167. package/tests/provider-contract-mirror.test.ts +206 -0
  168. package/tests/provider-defect-handoff.test.ts +252 -0
  169. package/tests/quiet-tool-rendering.test.ts +1055 -28
  170. package/tests/review-actor-tool-deny.test.ts +12 -13
  171. package/tests/review-authority-recovery-docs.test.ts +1 -2
  172. package/tests/review-candidate-view.test.ts +898 -12
  173. package/tests/review-compact-contract.test.ts +29 -122
  174. package/tests/review-controller-lock-status.test.ts +2 -2
  175. package/tests/review-controller-native-recovery.test.ts +366 -857
  176. package/tests/review-controller-native-routing.test.ts +1253 -4199
  177. package/tests/review-controller-retired-ops.test.ts +1 -1
  178. package/tests/review-controller-workspace-root.test.ts +236 -70
  179. package/tests/review-controller.test.ts +26 -816
  180. package/tests/review-corrected-finalize-binding.test.ts +134 -0
  181. package/tests/review-dispatch-hydration-gap.test.ts +145 -0
  182. package/tests/review-gate.test.ts +0 -45
  183. package/tests/review-host-relay-restart-parity.test.ts +360 -0
  184. package/tests/review-host-relay-routing.test.ts +352 -0
  185. package/tests/review-host-relay.test.ts +754 -0
  186. package/tests/review-integration-v2-forward.test.ts +654 -0
  187. package/tests/review-integration-v2.test.ts +317 -129
  188. package/tests/review-last-event-closure.test.ts +408 -0
  189. package/tests/review-ledger-contract.test.ts +106 -60
  190. package/tests/review-recovered-lineage-routing.test.ts +199 -0
  191. package/tests/review-relay-transport-agent.test.ts +322 -0
  192. package/tests/review-snapshot.test.ts +3 -2
  193. package/tests/runtime-harness.mjs +573 -180
  194. package/tests/sdd-agent-tools.test.ts +53 -32
  195. package/tests/sdd-preflight.test.ts +81 -15
  196. package/tests/sdd-status.test.ts +109 -110
  197. package/tests/skill-collision-prefixes.test.ts +6 -8
  198. package/tests/skill-registry.test.ts +50 -1
  199. package/tests/verify-package-files.test.ts +62 -0
  200. package/tests/writer-edit-surface-scope.test.ts +230 -0
  201. package/themes/Gentleman-Cute.json +94 -0
  202. package/themes/Gentleman-Sexy.json +92 -0
  203. package/assets/agents/review-refuter.md +0 -40
  204. package/assets/agents/review-validator.md +0 -23
  205. package/lib/git-commit-transaction.ts +0 -801
  206. package/lib/native-review-remediation.ts +0 -49
  207. package/lib/review-compact.ts +0 -947
  208. package/lib/review-refuter-adapter.ts +0 -129
  209. package/lib/review-runtime-contract.ts +0 -68
  210. package/prompts/gcl.md +0 -54
  211. package/prompts/gis.md +0 -25
  212. package/prompts/gpr.md +0 -41
  213. package/prompts/gwr.md +0 -31
  214. package/runtime/git-commit-transaction.mjs +0 -802
  215. package/scripts/run-git-commit-transaction.mjs +0 -35
  216. package/tests/fixtures/native-review-cli/v2.1.2/bind-sdd.json +0 -25
  217. package/tests/fixtures/native-review-cli/v2.1.2/finalize.json +0 -8
  218. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status-engram.json +0 -139
  219. package/tests/fixtures/native-review-cli/v2.1.2/sdd-status.json +0 -200
  220. package/tests/fixtures/native-review-cli/v2.1.2/start.json +0 -12
  221. package/tests/fixtures/native-review-cli/v2.1.2/validate-allow.json +0 -24
  222. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny-empty-context.json +0 -20
  223. package/tests/fixtures/native-review-cli/v2.1.2/validate-deny.json +0 -28
  224. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status-engram.json +0 -139
  225. package/tests/fixtures/native-review-cli/v2.1.3/sdd-status.json +0 -200
  226. package/tests/git-commit-transaction.test.ts +0 -302
  227. package/tests/review-compact.test.ts +0 -243
  228. package/tests/review-refuter-adapter.test.ts +0 -89
@@ -0,0 +1,264 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import askUserChoice from "../extensions/ask-user-choice.ts";
4
+
5
+ interface ChoiceResult {
6
+ content: Array<{ type: string; text: string }>;
7
+ details: Record<string, unknown>;
8
+ }
9
+
10
+ interface ChoiceOptionSchema {
11
+ additionalProperties?: boolean;
12
+ properties?: Record<string, unknown>;
13
+ }
14
+
15
+ interface ChoiceParameters {
16
+ additionalProperties?: boolean;
17
+ properties?: {
18
+ question?: unknown;
19
+ options?: {
20
+ minItems?: number;
21
+ maxItems?: number;
22
+ items?: ChoiceOptionSchema;
23
+ };
24
+ };
25
+ }
26
+
27
+ interface ChoiceTool {
28
+ name: string;
29
+ parameters: ChoiceParameters;
30
+ execute: (...args: unknown[]) => Promise<ChoiceResult>;
31
+ }
32
+
33
+ interface ChoiceLifecycleEvent {
34
+ channel: string;
35
+ data: { active: boolean };
36
+ }
37
+
38
+ type BeforeAgentStart = (event: unknown, ctx: { mode: string }) => void | Promise<void>;
39
+
40
+ function registerChoiceTool(
41
+ initialTools: string[] = [],
42
+ onLifecycleEvent?: (event: ChoiceLifecycleEvent) => void,
43
+ ) {
44
+ let activeTools = initialTools;
45
+ let runtimeActionsAllowed = false;
46
+ let getActiveToolsCalls = 0;
47
+ let setActiveToolsCalls = 0;
48
+ let tool: ChoiceTool | undefined;
49
+ const hooks: BeforeAgentStart[] = [];
50
+ const registeredToolNames: string[] = [];
51
+ const emittedEvents: ChoiceLifecycleEvent[] = [];
52
+ const pi = {
53
+ getActiveTools: () => {
54
+ getActiveToolsCalls++;
55
+ if (!runtimeActionsAllowed) {
56
+ throw new Error("runtime actions are unavailable while the extension is loading");
57
+ }
58
+ return activeTools;
59
+ },
60
+ setActiveTools: (names: string[]) => {
61
+ setActiveToolsCalls++;
62
+ if (!runtimeActionsAllowed) {
63
+ throw new Error("runtime actions are unavailable while the extension is loading");
64
+ }
65
+ activeTools = names;
66
+ },
67
+ registerTool: (candidate: unknown) => {
68
+ tool = candidate as ChoiceTool;
69
+ registeredToolNames.push(tool.name);
70
+ },
71
+ on: (event: string, handler: BeforeAgentStart) => {
72
+ if (event === "before_agent_start") hooks.push(handler);
73
+ },
74
+ events: {
75
+ emit(channel: string, data: { active: boolean }) {
76
+ const event = { channel, data };
77
+ emittedEvents.push(event);
78
+ onLifecycleEvent?.(event);
79
+ },
80
+ },
81
+ };
82
+ askUserChoice(pi as never);
83
+ assert.ok(tool, "ask_user_choice must register");
84
+ return {
85
+ hooks,
86
+ tool,
87
+ registeredToolNames,
88
+ activeTools: () => [...activeTools],
89
+ emittedEvents: () => [...emittedEvents],
90
+ runtimeActionCalls: () => ({ getActiveTools: getActiveToolsCalls, setActiveTools: setActiveToolsCalls }),
91
+ allowRuntimeActions: () => {
92
+ runtimeActionsAllowed = true;
93
+ },
94
+ };
95
+ }
96
+
97
+ const options = [
98
+ { label: "Authorize observed hash", description: "Accept the baseline hash observed in this runtime.", value: "authorize_observed_hash" },
99
+ { label: "Preserve requested hash", description: "Keep the hash from the original request.", value: "preserve_requested_hash" },
100
+ ];
101
+
102
+ function tuiContext(inputs: readonly string[], rendered: { value: string }) {
103
+ return {
104
+ mode: "tui",
105
+ ui: {
106
+ custom: async (factory: (tui: { requestRender(): void }, theme: { fg(_color: string, text: string): string; bold(text: string): string }, keybindings: unknown, done: (value: unknown) => void) => { render(width: number): string[]; handleInput(data: string): void }) => {
107
+ let result: unknown;
108
+ const component = factory(
109
+ { requestRender() {} },
110
+ { fg: (_color, text) => text, bold: (text) => text },
111
+ {},
112
+ (value) => {
113
+ result = value;
114
+ },
115
+ );
116
+ rendered.value = component.render(100).join("\n");
117
+ for (const input of inputs) component.handleInput(input);
118
+ return result;
119
+ },
120
+ },
121
+ };
122
+ }
123
+
124
+ test("ask_user_choice registers without runtime actions or overriding the open questionnaire", () => {
125
+ const registration = registerChoiceTool(["read", "ask_user_question"]);
126
+
127
+ assert.deepEqual(registration.registeredToolNames, ["ask_user_choice"]);
128
+ assert.deepEqual(registration.runtimeActionCalls(), { getActiveTools: 0, setActiveTools: 0 });
129
+ assert.deepEqual(registration.activeTools(), ["read", "ask_user_question"]);
130
+ });
131
+
132
+ test("ask_user_choice exposes a strict closed single-select schema", () => {
133
+ const { tool } = registerChoiceTool();
134
+ const optionsSchema = tool.parameters.properties?.options;
135
+ const optionSchema = optionsSchema?.items;
136
+
137
+ assert.equal(tool.name, "ask_user_choice");
138
+ assert.equal(tool.parameters.additionalProperties, false);
139
+ assert.deepEqual(Object.keys(tool.parameters.properties ?? {}).sort(), ["options", "question"]);
140
+ assert.equal(optionsSchema?.minItems, 2);
141
+ assert.equal(optionsSchema?.maxItems, 4);
142
+ assert.equal(optionSchema?.additionalProperties, false);
143
+ assert.deepEqual(Object.keys(optionSchema?.properties ?? {}).sort(), ["description", "label", "value"]);
144
+ });
145
+
146
+ test("ask_user_choice handles a closed Kilo hash decision with an opaque envelope value", async () => {
147
+ const { tool } = registerChoiceTool(["read"]);
148
+ const rendered = { value: "" };
149
+ const result = await tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, tuiContext(["\x1b[B", "\r"], rendered));
150
+ assert.match(rendered.value, /Proceed\?|Authorize observed hash|Accept the baseline hash|Preserve requested hash|Keep the hash/);
151
+ assert.doesNotMatch(rendered.value, /Type something|authorize_observed_hash|preserve_requested_hash/);
152
+ assert.deepEqual(result.details.selection, { value: "preserve_requested_hash", label: "Preserve requested hash", index: 2 });
153
+ assert.equal(result.content[0]?.text, "User selected: 2. Preserve requested hash (value: preserve_requested_hash)");
154
+ });
155
+
156
+ test("ask_user_choice cancels without a value and remains unavailable outside the TUI", async () => {
157
+ const { tool } = registerChoiceTool();
158
+ const rendered = { value: "" };
159
+ const cancelled = await tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, tuiContext(["\x1b"], rendered));
160
+ assert.equal(cancelled.details.selection, undefined);
161
+ assert.equal(cancelled.details.cancelled, true);
162
+ await assert.rejects(
163
+ () => tool.execute("call", { question: "Proceed?", options }, new AbortController().signal, undefined, { mode: "print" }),
164
+ /unavailable outside the interactive TUI/,
165
+ );
166
+ });
167
+
168
+ test("ask_user_choice emits a private balanced lifecycle around selection and cancellation", async () => {
169
+ const sequence: string[] = [];
170
+ const selectedRegistration = registerChoiceTool([], ({ data }) => {
171
+ sequence.push(data.active ? "active" : "inactive");
172
+ });
173
+ const selected = await selectedRegistration.tool.execute(
174
+ "call",
175
+ { question: "private choice question", options },
176
+ new AbortController().signal,
177
+ undefined,
178
+ {
179
+ mode: "tui",
180
+ ui: {
181
+ custom: async () => {
182
+ sequence.push("custom");
183
+ return { value: "preserve_requested_hash", label: "Preserve requested hash", index: 2 };
184
+ },
185
+ },
186
+ },
187
+ );
188
+ assert.equal(selected.details.selection?.value, "preserve_requested_hash");
189
+ assert.deepEqual(sequence, ["active", "custom", "inactive"]);
190
+ assert.deepEqual(selectedRegistration.emittedEvents(), [
191
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: true } },
192
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: false } },
193
+ ]);
194
+ assert.doesNotMatch(
195
+ JSON.stringify(selectedRegistration.emittedEvents()),
196
+ /private choice question|Authorize observed hash|Accept the baseline hash|preserve_requested_hash/,
197
+ );
198
+
199
+ const cancelledRegistration = registerChoiceTool();
200
+ const cancelled = await cancelledRegistration.tool.execute(
201
+ "call",
202
+ { question: "Proceed?", options },
203
+ new AbortController().signal,
204
+ undefined,
205
+ { mode: "tui", ui: { custom: async () => undefined } },
206
+ );
207
+ assert.equal(cancelled.details.cancelled, true);
208
+ assert.deepEqual(cancelledRegistration.emittedEvents(), [
209
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: true } },
210
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: false } },
211
+ ]);
212
+ });
213
+
214
+ test("ask_user_choice settles its lifecycle after a custom UI error and emits nothing outside the TUI", async () => {
215
+ const failedRegistration = registerChoiceTool();
216
+ const customError = new Error("custom UI failed");
217
+ await assert.rejects(
218
+ failedRegistration.tool.execute(
219
+ "call",
220
+ { question: "Proceed?", options },
221
+ new AbortController().signal,
222
+ undefined,
223
+ { mode: "tui", ui: { custom: async () => { throw customError; } } },
224
+ ),
225
+ (error) => error === customError,
226
+ );
227
+ assert.deepEqual(failedRegistration.emittedEvents(), [
228
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: true } },
229
+ { channel: "gentle-pi:ask-user-choice:blocked", data: { active: false } },
230
+ ]);
231
+
232
+ const nonTuiRegistration = registerChoiceTool();
233
+ let customCalled = false;
234
+ await assert.rejects(
235
+ nonTuiRegistration.tool.execute(
236
+ "call",
237
+ { question: "Proceed?", options },
238
+ new AbortController().signal,
239
+ undefined,
240
+ {
241
+ mode: "print",
242
+ ui: {
243
+ custom: async () => {
244
+ customCalled = true;
245
+ return undefined;
246
+ },
247
+ },
248
+ },
249
+ ),
250
+ /unavailable outside the interactive TUI/,
251
+ );
252
+ assert.equal(customCalled, false);
253
+ assert.deepEqual(nonTuiRegistration.emittedEvents(), []);
254
+ });
255
+
256
+ test("ask_user_choice is offered only for interactive TUI turns and preserves the open questionnaire", async () => {
257
+ const registration = registerChoiceTool(["read", "ask_user_question"]);
258
+ registration.allowRuntimeActions();
259
+
260
+ for (const hook of registration.hooks) await hook({}, { mode: "tui" });
261
+ assert.deepEqual(registration.activeTools(), ["read", "ask_user_question", "ask_user_choice"]);
262
+ for (const hook of registration.hooks) await hook({}, { mode: "print" });
263
+ assert.deepEqual(registration.activeTools(), ["read", "ask_user_question"]);
264
+ });