gentle-pi 2.5.0 → 2.6.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 (189) hide show
  1. package/README.md +139 -30
  2. package/assets/agents/gentle-ai-worker.md +4 -0
  3. package/assets/agents/jd-fix-agent.md +18 -0
  4. package/assets/agents/jd-judge-a.md +1 -1
  5. package/assets/agents/jd-judge-b.md +1 -1
  6. package/assets/agents/sdd-apply.md +7 -5
  7. package/assets/agents/sdd-archive.md +5 -3
  8. package/assets/agents/sdd-design.md +4 -0
  9. package/assets/agents/sdd-explore.md +4 -0
  10. package/assets/agents/sdd-init.md +4 -0
  11. package/assets/agents/sdd-onboard.md +4 -0
  12. package/assets/agents/sdd-proposal.md +4 -0
  13. package/assets/agents/sdd-remediate.md +37 -0
  14. package/assets/agents/sdd-research.md +26 -3
  15. package/assets/agents/sdd-spec.md +4 -0
  16. package/assets/agents/sdd-status.md +9 -75
  17. package/assets/agents/sdd-sync.md +4 -0
  18. package/assets/agents/sdd-tasks.md +4 -0
  19. package/assets/agents/sdd-verify.md +5 -3
  20. package/assets/chains/sdd-full.chain.md +4 -0
  21. package/assets/chains/sdd-plan.chain.md +4 -0
  22. package/assets/chains/sdd-verify.chain.md +4 -0
  23. package/assets/migrations/managed-assets-v2.5.0.json +7 -0
  24. package/assets/orchestrator-delegation.md +21 -3
  25. package/assets/sdd-orchestrator-workflow.md +54 -21
  26. package/assets/support/sdd-status-contract.md +34 -90
  27. package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
  28. package/docs/telemetry.md +57 -1
  29. package/docs/windows-startup-console-visibility.md +18 -0
  30. package/extensions/ask-user-choice.ts +143 -15
  31. package/extensions/codegraph-tools.ts +1 -0
  32. package/extensions/gentle-agents.ts +799 -50
  33. package/extensions/gentle-ai.ts +2033 -322
  34. package/extensions/gentle-shell.ts +145 -42
  35. package/extensions/gentle-todo.ts +47 -12
  36. package/extensions/quiet-tools.ts +1 -0
  37. package/extensions/runtime-metrics.ts +130 -0
  38. package/extensions/sdd-init.ts +2 -2
  39. package/extensions/startup-banner.ts +52 -75
  40. package/lib/agent-profiles.ts +550 -0
  41. package/lib/agents-completion-delivery.ts +72 -0
  42. package/lib/agents-config.ts +7 -10
  43. package/lib/agents-history.ts +9 -1
  44. package/lib/agents-messaging.ts +187 -0
  45. package/lib/agents-protocol.ts +77 -5
  46. package/lib/agents-runner.ts +548 -26
  47. package/lib/agents-thread-view.ts +57 -0
  48. package/lib/agents-view-layout.ts +40 -0
  49. package/lib/agents-view.ts +548 -191
  50. package/lib/agents-widget.ts +33 -14
  51. package/lib/gentle-ai-binary.ts +3 -1
  52. package/lib/gentle-ai-renderer.ts +8 -6
  53. package/lib/native-review-cli.ts +283 -1
  54. package/lib/orchestrator-presence.ts +337 -0
  55. package/lib/profiles-orchestrator.ts +203 -0
  56. package/lib/review-candidate-view-owner.ts +296 -46
  57. package/lib/review-candidate-view.ts +30 -20
  58. package/lib/review-consent-component.ts +247 -0
  59. package/lib/review-consent-ui.ts +53 -8
  60. package/lib/review-host-relay.ts +28 -0
  61. package/lib/review-integration-v2.ts +187 -5
  62. package/lib/review-last-event-controller.ts +7 -4
  63. package/lib/review-reminder-receipt.ts +74 -0
  64. package/lib/review-session-standing-permission.ts +27 -6
  65. package/lib/runtime-metrics-children.ts +199 -0
  66. package/lib/runtime-metrics-delivery.ts +68 -0
  67. package/lib/runtime-metrics-native.ts +166 -0
  68. package/lib/runtime-metrics-pi-identity.ts +113 -0
  69. package/lib/runtime-metrics-policy.ts +51 -0
  70. package/lib/runtime-metrics.ts +255 -0
  71. package/lib/sdd-preflight.ts +362 -81
  72. package/lib/sdd-research-capabilities.ts +228 -0
  73. package/lib/sdd-status.ts +29 -7
  74. package/lib/session-worktree-registry.ts +118 -0
  75. package/lib/shell-bar.ts +47 -1
  76. package/lib/shell-card.ts +1 -4
  77. package/lib/shell-changes-view.ts +362 -37
  78. package/lib/shell-changes.ts +81 -1
  79. package/lib/shell-prompt.ts +11 -15
  80. package/lib/shell-sidebar-banner.ts +11 -0
  81. package/lib/shell-sidebar-layout.ts +213 -0
  82. package/lib/shell-sidebar.ts +41 -0
  83. package/lib/shell-todo.ts +28 -11
  84. package/lib/telemetry-trigger.ts +2 -0
  85. package/package.json +6 -3
  86. package/runtime/gentle-ai-binary.mjs +3 -1
  87. package/runtime/native-review-cli.mjs +283 -1
  88. package/runtime/review-integration-v2.mjs +187 -5
  89. package/runtime/telemetry-trigger.mjs +2 -0
  90. package/scripts/build-runtime-modules.mjs +9 -1
  91. package/scripts/check-types.mjs +125 -0
  92. package/scripts/gentle-ai-installer.mjs +10 -10
  93. package/scripts/install-gentle-ai.mjs +12 -0
  94. package/scripts/install-tui-mode-setting.mjs +114 -0
  95. package/scripts/test-packed-runner.mjs +16 -2
  96. package/scripts/types-baseline.json +99 -0
  97. package/scripts/verify-package-files.mjs +4 -2
  98. package/skills/_shared/review-ledger-contract.md +17 -1
  99. package/skills/issue-creation/SKILL.md +3 -3
  100. package/skills/judgment-day/SKILL.md +17 -3
  101. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  102. package/tests/agent-profiles.test.ts +722 -0
  103. package/tests/agents-completion-delivery.test.ts +94 -0
  104. package/tests/agents-config.test.ts +62 -0
  105. package/tests/agents-fake-child.ts +15 -1
  106. package/tests/agents-grouping.test.ts +179 -0
  107. package/tests/agents-integration.test.ts +100 -0
  108. package/tests/agents-messaging.test.ts +94 -0
  109. package/tests/agents-protocol.test.ts +45 -0
  110. package/tests/agents-queries.test.ts +190 -0
  111. package/tests/agents-responsive.test.ts +43 -0
  112. package/tests/agents-runner.test.ts +571 -14
  113. package/tests/agents-thread-view.test.ts +45 -0
  114. package/tests/agents-view.test.ts +476 -65
  115. package/tests/agents-widget.test.ts +31 -1
  116. package/tests/artifact-language.test.ts +25 -2
  117. package/tests/ask-user-choice.test.ts +169 -3
  118. package/tests/asset-installation-runtime.test.ts +108 -0
  119. package/tests/autonomous-guard.test.ts +116 -1
  120. package/tests/codegraph-tools.test.ts +2 -1
  121. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  122. package/tests/devbinary/native-review-parity.devtest.ts +2 -0
  123. package/tests/feature-request-form.test.ts +67 -0
  124. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  125. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  126. package/tests/gentle-agents.test.ts +1460 -33
  127. package/tests/gentle-ai-binary.test.ts +7 -2
  128. package/tests/gentle-ai-installer.test.ts +47 -47
  129. package/tests/gentle-ai-renderer.test.ts +38 -0
  130. package/tests/gentle-ai.test.ts +944 -4
  131. package/tests/gentle-shell.test.ts +303 -12
  132. package/tests/gentle-todo.test.ts +54 -10
  133. package/tests/install-tui-mode-setting.test.ts +324 -0
  134. package/tests/issue-creation-skill.test.ts +22 -0
  135. package/tests/model-routing-authority.test.ts +12 -0
  136. package/tests/native-review-capability-contract.test.ts +12 -1
  137. package/tests/native-review-cli.test.ts +277 -3
  138. package/tests/native-review-parity.test.ts +14 -7
  139. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  140. package/tests/orchestrator-presence.test.ts +389 -0
  141. package/tests/package-manifest.test.ts +232 -7
  142. package/tests/profiles-orchestrator.test.ts +208 -0
  143. package/tests/quiet-tool-rendering.test.ts +1 -0
  144. package/tests/rdd-aware-verification-contract.test.ts +10 -0
  145. package/tests/review-agent-end-preflight.test.ts +332 -24
  146. package/tests/review-candidate-view.test.ts +304 -6
  147. package/tests/review-consent-ui.test.ts +352 -0
  148. package/tests/review-contract-prompt.test.ts +14 -0
  149. package/tests/review-controller-native-routing.test.ts +563 -3
  150. package/tests/review-controller.test.ts +1 -1
  151. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  152. package/tests/review-host-relay-routing.test.ts +364 -4
  153. package/tests/review-host-relay.test.ts +29 -0
  154. package/tests/review-integration-v2-forward.test.ts +44 -0
  155. package/tests/review-integration-v2.test.ts +164 -0
  156. package/tests/review-last-event-closure.test.ts +105 -1
  157. package/tests/review-ledger-contract.test.ts +61 -6
  158. package/tests/review-reminder-receipt.test.ts +62 -0
  159. package/tests/review-session-standing-permission-controller.test.ts +52 -4
  160. package/tests/review-session-standing-permission.test.ts +30 -0
  161. package/tests/runtime-harness.mjs +447 -39
  162. package/tests/runtime-metrics-children.test.ts +206 -0
  163. package/tests/runtime-metrics-delivery.test.ts +85 -0
  164. package/tests/runtime-metrics-extension.test.ts +187 -0
  165. package/tests/runtime-metrics-native.test.ts +209 -0
  166. package/tests/runtime-metrics-pi-identity.test.ts +113 -0
  167. package/tests/runtime-metrics-policy.test.ts +62 -0
  168. package/tests/runtime-metrics.test.ts +184 -0
  169. package/tests/sdd-agent-tools.test.ts +10 -1
  170. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  171. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  172. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  173. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  174. package/tests/sdd-preflight.test.ts +252 -8
  175. package/tests/sdd-research-capabilities.test.ts +256 -0
  176. package/tests/sdd-research-live.test.ts +241 -0
  177. package/tests/sdd-selection-transport.test.ts +504 -0
  178. package/tests/sdd-status.test.ts +51 -0
  179. package/tests/session-worktree-registry.test.ts +135 -0
  180. package/tests/shell-card.test.ts +24 -3
  181. package/tests/shell-changes-view.test.ts +471 -8
  182. package/tests/shell-changes.test.ts +168 -0
  183. package/tests/shell-prompt.test.ts +28 -6
  184. package/tests/shell-sidebar-banner.test.ts +23 -0
  185. package/tests/shell-sidebar-layout.test.ts +387 -0
  186. package/tests/shell-sidebar.test.ts +50 -0
  187. package/tests/shell-todo.test.ts +100 -11
  188. package/tests/startup-banner.test.ts +126 -0
  189. package/tests/telemetry-trigger.test.ts +3 -1
@@ -0,0 +1,352 @@
1
+ import assert from "node:assert/strict";
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import test from "node:test";
5
+ import { visibleWidth } from "@earendil-works/pi-tui";
6
+ import { ReviewConsentComponent } from "../lib/review-consent-component.ts";
7
+ import { formatReviewConsentUi } from "../lib/review-consent-ui.ts";
8
+ import { decodeReviewConsentV3 } from "../lib/review-integration-v2.ts";
9
+
10
+ function consent() {
11
+ const path = join(process.cwd(), "tests", "fixtures", "devbinary", "consent-v3.captured.json");
12
+ const raw = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
13
+ raw.agent = "pi";
14
+ return formatReviewConsentUi(decodeReviewConsentV3(raw, "pi"));
15
+ }
16
+
17
+ function createComponent(
18
+ rows: number,
19
+ selected: number[] = [],
20
+ cancelled: number[] = [],
21
+ content = consent().content,
22
+ ) {
23
+ return new ReviewConsentComponent({
24
+ content,
25
+ theme: { fg: (_color, text) => text, bold: (text) => text },
26
+ keybindings: undefined,
27
+ terminalRows: () => rows,
28
+ onSelect: (index) => selected.push(index),
29
+ onCancel: () => cancelled.push(1),
30
+ });
31
+ }
32
+
33
+ function joined(lines: string[]): string {
34
+ return lines.join(" ").replaceAll("→ ", "").replace(/\s+/g, " ");
35
+ }
36
+
37
+ function assertBounded(lines: string[], width: number, height: number): void {
38
+ assert.ok(lines.length <= height, `${width}x${height} must fit its terminal`);
39
+ for (const line of lines) assert.ok(visibleWidth(line) <= width, `${width}: ${line}`);
40
+ }
41
+
42
+ test("primary consent is compact by default and opens exact details only with D", () => {
43
+ const base = consent().content;
44
+ const content = {
45
+ ...base,
46
+ headline: "Review this change now?",
47
+ reason: "This is the exact provider reason.",
48
+ actions: [
49
+ { label: "Review this change", effect: "Reviews only this change; delivery stays separate." },
50
+ { label: "Skip this time", effect: "Skips only this change; no preference is saved." },
51
+ { label: "Review and allow this session", effect: "Reviews this change and permits later session reviews." },
52
+ ] as typeof base.actions,
53
+ };
54
+ const selected: number[] = [];
55
+ const component = createComponent(24, selected, [], content);
56
+ const primary = component.render(80);
57
+ assertBounded(primary, 80, 24);
58
+ assert.deepEqual(component.render(80), primary, "80x24 is stable without terminal-height padding");
59
+ assert.deepEqual(createComponent(40, [], [], content).render(80), primary, "80x40 keeps the same compact primary content");
60
+ assert.ok(primary.length >= 9 && primary.length <= 12, "the concise fixture keeps its provider reason within the primary row budget");
61
+ assert.ok(joined(primary).includes(content.headline));
62
+ assert.ok(joined(primary).includes(content.reason), "the exact provider reason is visible without opening details");
63
+ assert.ok(!joined(primary).includes("CONTEXT"));
64
+ assert.ok(!joined(primary).includes("Provider headline:"));
65
+ for (const action of content.actions) assert.ok(joined(primary).includes(action.label));
66
+ assert.ok(joined(primary).includes(content.actions[0].effect));
67
+ assert.ok(!joined(primary).includes(content.actions[1].effect));
68
+ assert.match(joined(primary), /Enter confirm.*Esc cancel.*D details/);
69
+ component.handleInput("d");
70
+ const details = component.render(80);
71
+ assert.ok(joined(details).includes("Details (read-only)"));
72
+ assert.ok(joined(details).includes(content.reason));
73
+ assert.ok(joined(details).includes("Provider headline:"));
74
+ component.handleInput("\r");
75
+ assert.deepEqual(selected, [], "details cannot confirm");
76
+ component.handleInput("D");
77
+ component.handleInput("\r");
78
+ assert.deepEqual(selected, [], "returning to main recomputes its visible-confirm guard");
79
+ component.render(80);
80
+ component.handleInput("\r");
81
+ assert.deepEqual(selected, [0]);
82
+ });
83
+
84
+ test("context and complete effects precede enabled consent, while short terminals stay safe", () => {
85
+ const selected: number[] = [];
86
+ const full = createComponent(24, selected);
87
+ const initial = full.render(80);
88
+ const content = consent().content;
89
+ assertBounded(initial, 80, 24);
90
+ for (const text of [content.headline, content.actions[0].effect]) {
91
+ assert.ok(joined(initial).includes(text), `initial screen preserves ${text}`);
92
+ }
93
+ assert.ok(joined(initial).includes(content.reason), "primary keeps the exact provider reason visible");
94
+ assert.ok(!joined(initial).includes("CONTEXT"), "primary has no permanent context block");
95
+ full.handleInput("\r");
96
+ assert.deepEqual(selected, [0], "a complete visible grant may be confirmed");
97
+
98
+ for (const width of [20, 40, 80]) {
99
+ const shortSelected: number[] = [];
100
+ const cancelled: number[] = [];
101
+ const short = createComponent(7, shortSelected, cancelled);
102
+ const view = short.render(width);
103
+ assertBounded(view, width, 7);
104
+ assert.ok(joined(view).includes("Action 1/3"), "safe view names the unavailable action without partial labels");
105
+ assert.ok(joined(view).includes("Resize; Esc cancel; Enter off"), "safe view explains disabled confirmation");
106
+ short.handleInput("\r");
107
+ short.handleInput("\u001b");
108
+ assert.deepEqual(shortSelected, [], "compact screens never hide effects then confirm");
109
+ assert.deepEqual(cancelled, [1], "compact screens retain cancellation");
110
+ }
111
+
112
+ const tiny = createComponent(4);
113
+ const tinyView = tiny.render(20);
114
+ assertBounded(tinyView, 20, 4);
115
+ assert.ok(joined(tinyView).includes("Resize; Esc cancel; Enter off"), "tiny fallback leads with resize and cancellation guidance");
116
+ const oneRowCancelled: number[] = [];
117
+ const oneRow = createComponent(1, [], oneRowCancelled);
118
+ assert.ok(joined(oneRow.render(20)).includes("Resize; Esc cancel"), "one-row fallback leads with resize and cancellation guidance");
119
+ oneRow.handleInput("\u001b");
120
+ assert.deepEqual(oneRowCancelled, [1], "one-row fallback retains cancellation");
121
+ });
122
+
123
+ test("primary preserves provider risk reasons without paraphrasing", () => {
124
+ const base = consent().content;
125
+ for (const reason of [
126
+ "These changes may affect execution; review can help detect problems.",
127
+ "Security-sensitive changes need an authoritative review decision.",
128
+ "Behavior updates may warrant review before delivery.",
129
+ ]) {
130
+ const primary = createComponent(24, [], [], { ...base, reason }).render(80);
131
+ assert.ok(joined(primary).includes(reason), `80x24 shows the exact provider reason: ${reason}`);
132
+ assert.ok(!joined(primary).includes("Reason:"), "primary does not add a context label around the reason");
133
+ }
134
+ });
135
+
136
+ test("selection anchors fitting effects despite long context and invalidates rapid confirmation", () => {
137
+ const base = consent().content;
138
+ const content = {
139
+ ...base,
140
+ headline: "Question?",
141
+ reason: "long provider context ".repeat(200),
142
+ actions: [
143
+ { label: "one", effect: "one effect" },
144
+ { label: "two", effect: "two effect" },
145
+ { label: "three", effect: "three effect" },
146
+ ] as typeof base.actions,
147
+ };
148
+ const anchorActions = (width: number) => {
149
+ const selected: number[] = [];
150
+ const component = createComponent(24, selected, [], content);
151
+ assert.ok(!joined(component.render(width)).includes("long provider context"), `${width}x24 keeps provider context in details`);
152
+ assert.ok(joined(component.render(width)).includes(content.actions[0].effect), `${width}x24 keeps the selected consequence visible`);
153
+ component.handleInput("\r");
154
+ assert.deepEqual(selected, [0], "a separately visible complete action may confirm without scrolling the long context");
155
+ for (const [input, index, effect] of [
156
+ ["\u001b[A", 0, content.actions[0].effect],
157
+ ["\u001b[B", 1, content.actions[1].effect],
158
+ ["\u001b[B", 2, content.actions[2].effect],
159
+ ] as const) {
160
+ component.handleInput(input);
161
+ const before = [...selected];
162
+ component.handleInput("\r");
163
+ assert.deepEqual(selected, before, "rapid navigation invalidates confirmation");
164
+ assert.ok(joined(component.render(width)).includes(effect), `choice ${index + 1} anchors at ${width} columns`);
165
+ component.handleInput("\r");
166
+ assert.equal(selected.at(-1), index);
167
+ }
168
+ return { component, selected };
169
+ };
170
+ const { component, selected } = anchorActions(80);
171
+ anchorActions(40);
172
+ component.handleInput("d");
173
+ assert.ok(joined(component.render(80)).includes("long provider context"), "D opens complete provider context");
174
+ component.handleInput("D");
175
+ component.handleInput("\u001b[A");
176
+ component.render(60);
177
+ assert.ok(joined(component.render(80)).includes(content.actions[1].effect), "resize back anchors the selected consequence");
178
+
179
+ component.handleInput("d");
180
+ const details = component.render(12);
181
+ assertBounded(details, 12, 24);
182
+ component.handleInput("\r");
183
+ assert.deepEqual(selected, [0, 0, 1, 2], "details never confirm a hidden action");
184
+ let sawOffPath = false;
185
+ for (let row = 0; row < 200; row += 1) {
186
+ sawOffPath ||= joined(component.render(12)).includes("Off-path");
187
+ component.handleInput("\u001b[6~");
188
+ }
189
+ assert.ok(sawOffPath, "long fixture off-path details remain reachable");
190
+ const risk = createComponent(40);
191
+ risk.render(12);
192
+ risk.handleInput("d");
193
+ let sawRiskEvidence = false;
194
+ for (let row = 0; row < 200; row += 1) {
195
+ const riskDetails = risk.render(12);
196
+ assertBounded(riskDetails, 12, 40);
197
+ sawRiskEvidence ||= joined(riskDetails).includes("Risk evidence:");
198
+ risk.handleInput("\u001b[B");
199
+ }
200
+ assert.ok(sawRiskEvidence, "context scrolling exposes risk-evidence headings at twelve columns");
201
+ const original = consent().content;
202
+ const shortContent = {
203
+ ...original,
204
+ reason: "short provider context",
205
+ value: "short provider value",
206
+ actions: [
207
+ { label: "one", effect: "one" },
208
+ { label: "two", effect: "two" },
209
+ { label: "three", effect: "three" },
210
+ ] as typeof original.actions,
211
+ };
212
+ assertBounded(createComponent(40, [], [], shortContent).render(12), 12, 40);
213
+
214
+ const oneLineDetails = createComponent(4);
215
+ oneLineDetails.render(20);
216
+ oneLineDetails.handleInput("d");
217
+ const before = oneLineDetails.render(20);
218
+ oneLineDetails.handleInput("\u001b[6~");
219
+ assert.notDeepEqual(oneLineDetails.render(20), before, "one-line details still page forward");
220
+
221
+ const resized = createComponent(7);
222
+ resized.render(80);
223
+ resized.handleInput("\u001b[B");
224
+ resized.handleInput("\u001b[B");
225
+ const resizedView = resized.render(20);
226
+ assertBounded(resizedView, 20, 7);
227
+ assert.ok(joined(resizedView).includes("Action 3/3"), "resize keeps the selected action identifiable without partial labels");
228
+ assert.ok(joined(resizedView).includes("Resize; Esc cancel; Enter off"), "resize disables hidden confirmation with guidance");
229
+ });
230
+
231
+ test("fixed title separates an enormous provider context from anchored consent actions", () => {
232
+ const base = consent().content;
233
+ const content = {
234
+ ...base,
235
+ headline: "ENORMOUS PROVIDER HEADLINE: review authority for a frozen candidate needs explicit consent before any reviewer can inspect immutable evidence, with no summary substituted by Pi.",
236
+ reason: "The provider requires an explicit decision because the frozen candidate changes review-sensitive behavior and the user must see the complete reason before choosing.",
237
+ value: "A bounded review can identify candidate-caused defects while preserving ordinary repository delivery decisions and no automatic approval.",
238
+ evidence: [
239
+ "Risk evidence: the candidate changes authority-bearing review behavior.",
240
+ "Risk evidence: the provider recorded a scoped immutable target identity.",
241
+ ],
242
+ offPathNote: "OFF PATH UNIQUE: cancellation declines this prompt only and does not change the review switch.",
243
+ actions: [
244
+ { label: "Grant this frozen candidate review", effect: "GRANT UNIQUE: starts only this provider-bound review for the frozen candidate; it creates no delivery approval or standing permission." },
245
+ { label: "Decline this frozen candidate review", effect: "DECLINE UNIQUE: declines only this candidate; no review starts and no preference is saved." },
246
+ { label: "Grant one Pi-session review permission", effect: "SESSION UNIQUE: runs this exact provider grant, then permits later fresh validated candidates only for this Pi session and canonical repository identity; it grants no verdict, delivery, or cross-repository authority." },
247
+ ] as typeof base.actions,
248
+ };
249
+ const selected: number[] = [];
250
+ const component = createComponent(24, selected, [], content);
251
+ const initial = component.render(80);
252
+ assertBounded(initial, 80, 24);
253
+ assert.equal(initial[0], "Review consent", "the visual title is app-owned and never becomes the provider headline");
254
+ assert.ok(!initial.includes("CONTEXT"), "provider fields are absent from the default primary");
255
+ assert.ok(!joined(initial).includes("Provider headline:"), "the provider headline is not a primary option");
256
+ assert.ok(joined(initial).includes("GRANT UNIQUE"), "the selected action's complete consequence is readable beside the context viewport");
257
+ component.handleInput("d");
258
+ for (let row = 0; row < 200; row += 1) component.handleInput("\u001b[6~");
259
+ const scrolled = component.render(80);
260
+ assertBounded(scrolled, 80, 24);
261
+ assert.equal(scrolled[0], "Details (read-only)", "details use their own fixed read-only title");
262
+ assert.ok(scrolled.includes("Details (read-only)"), "details retain a read-only heading while scrolling");
263
+ assert.ok(joined(scrolled).includes("OFF PATH UNIQUE"), "the complete provider context is recoverable through its viewport");
264
+ component.handleInput("\r");
265
+ assert.deepEqual(selected, [], "context details cannot confirm an action");
266
+ component.handleInput("D");
267
+ component.handleInput("\u001b[B");
268
+ const second = component.render(80);
269
+ assert.ok(joined(second).includes("DECLINE UNIQUE"), "navigation anchors each selected action's exact consequence");
270
+ component.handleInput("\r");
271
+ assert.deepEqual(selected, [1], "a rendered complete selected action remains confirmable after context recovery");
272
+
273
+ const tallComponent = createComponent(40, [], [], content);
274
+ tallComponent.render(80);
275
+ tallComponent.handleInput("\u001b[B");
276
+ tallComponent.handleInput("\u001b[B");
277
+ const tall = tallComponent.render(80);
278
+ assertBounded(tall, 80, 40);
279
+ assert.equal(tall[0], "Review consent", "80x40 keeps the same fixed brief title");
280
+ assert.ok(joined(tall).includes("SESSION UNIQUE"), "80x40 exposes the realistic session-permission consequence in the action area");
281
+ });
282
+
283
+ test("fixed title uses a heading token without recoloring context or selection", () => {
284
+ const foregrounds: Array<{ color: string; text: string }> = [];
285
+ const component = new ReviewConsentComponent({
286
+ content: consent().content,
287
+ theme: {
288
+ fg: (color, text) => {
289
+ foregrounds.push({ color, text });
290
+ return text;
291
+ },
292
+ bold: (text) => `**${text}**`,
293
+ },
294
+ keybindings: undefined,
295
+ terminalRows: () => 24,
296
+ onSelect: () => {},
297
+ onCancel: () => {},
298
+ });
299
+ const lines = component.render(80);
300
+ assert.equal(lines[0], "**Review consent**", "fixed title retains bold styling");
301
+ assert.deepEqual(foregrounds.find(({ text }) => text === "**Review consent**"), {
302
+ color: "mdHeading",
303
+ text: "**Review consent**",
304
+ }, "fixed title uses the heading semantic token");
305
+ assert.ok(foregrounds.some(({ color, text }) => color === "accent" && text.startsWith("→ 1.")), "selected action remains accent styled");
306
+ assert.ok(!foregrounds.some(({ text }) => text.startsWith("Provider headline:")), "common body remains unstyled");
307
+ });
308
+
309
+ test("oversized selected consequences stay complete in read-only context without enabling consent", () => {
310
+ const base = consent().content;
311
+ const effects = [
312
+ "OVERSIZED ONE BEGIN " + "one-context-token ".repeat(140) + "OVERSIZED_ONE_COMPLETE_END",
313
+ "OVERSIZED TWO BEGIN " + "two-context-token ".repeat(140) + "OVERSIZED_TWO_COMPLETE_END",
314
+ "OVERSIZED THREE BEGIN " + "three-context-token ".repeat(140) + "OVERSIZED_THREE_COMPLETE_END",
315
+ ] as const;
316
+ const content = {
317
+ ...base,
318
+ actions: [
319
+ { label: "Oversized grant action", effect: effects[0] },
320
+ { label: "Oversized decline action", effect: effects[1] },
321
+ { label: "Oversized session action", effect: effects[2] },
322
+ ] as typeof base.actions,
323
+ };
324
+ for (const rows of [24, 40]) {
325
+ const selected: number[] = [];
326
+ const component = createComponent(rows, selected, [], content);
327
+ const initial = component.render(80);
328
+ assertBounded(initial, 80, rows);
329
+ assert.ok(joined(initial).includes("Action 1/3 needs its full effect"), `${rows} rows uses safe action guidance for an oversized effect`);
330
+ component.handleInput("\r");
331
+ assert.deepEqual(selected, [], `${rows} rows never confirms a hidden oversized effect`);
332
+ component.handleInput("d");
333
+ let sawActionConsequences = false;
334
+ const visited = new Set<string>();
335
+ for (let page = 0; page < 700; page += 1) {
336
+ const view = component.render(80);
337
+ assertBounded(view, 80, rows);
338
+ const text = joined(view);
339
+ sawActionConsequences ||= text.includes("Action consequences");
340
+ for (const marker of ["OVERSIZED ONE BEGIN", "OVERSIZED_ONE_COMPLETE_END", "OVERSIZED TWO BEGIN", "OVERSIZED_TWO_COMPLETE_END", "OVERSIZED THREE BEGIN", "OVERSIZED_THREE_COMPLETE_END"]) {
341
+ if (text.includes(marker)) visited.add(marker);
342
+ }
343
+ component.handleInput("\u001b[B");
344
+ }
345
+ assert.ok(sawActionConsequences, `${rows} rows labels read-only action consequences in context`);
346
+ assert.deepEqual([...visited].sort(), ["OVERSIZED ONE BEGIN", "OVERSIZED_ONE_COMPLETE_END", "OVERSIZED THREE BEGIN", "OVERSIZED_THREE_COMPLETE_END", "OVERSIZED TWO BEGIN", "OVERSIZED_TWO_COMPLETE_END"].sort(), `${rows} rows exposes every oversized effect boundary through context paging`);
347
+ const afterDetails = component.render(80);
348
+ assert.ok(afterDetails.includes("Details (read-only)"), `${rows} rows retains a read-only details heading while paging`);
349
+ component.handleInput("\r");
350
+ assert.deepEqual(selected, [], `${rows} rows keeps details read-only after paging`);
351
+ }
352
+ });
@@ -93,6 +93,20 @@ test("before_agent_start does not inject the review execution contract for a nam
93
93
  assert.doesNotMatch(result.systemPrompt, /Gentle AI review execution contract/);
94
94
  });
95
95
 
96
+ test("before_agent_start does not inject the review execution contract for gentle-ai-worker", async () => {
97
+ const { beforeAgentStart } = harness({} as NativeReviewCli);
98
+ const result = await beforeAgentStart({ agentName: "gentle-ai-worker", systemPrompt: "base" }, ctx());
99
+ assert.equal(result.systemPrompt, "base");
100
+ assert.doesNotMatch(result.systemPrompt, /Gentle AI review execution contract/);
101
+ });
102
+
103
+ test("before_agent_start does not inject the review execution contract for jd-fix-agent", async () => {
104
+ const { beforeAgentStart } = harness({} as NativeReviewCli);
105
+ const result = await beforeAgentStart({ agentName: "jd-fix-agent", systemPrompt: "base" }, ctx());
106
+ assert.equal(result.systemPrompt, "base");
107
+ assert.doesNotMatch(result.systemPrompt, /Gentle AI review execution contract/);
108
+ });
109
+
96
110
  test("before_agent_start does not inject the review execution contract for an SDD executor session", async () => {
97
111
  const { beforeAgentStart } = harness({} as NativeReviewCli);
98
112
  const result = await beforeAgentStart({ systemPrompt: "SDD apply executor body" }, ctx());