gentle-pi 2.3.0 → 2.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 (153) hide show
  1. package/README.md +195 -11
  2. package/assets/agents/gentle-ai-worker.md +9 -0
  3. package/assets/agents/sdd-explore.md +1 -0
  4. package/assets/orchestrator-delegation.md +21 -10
  5. package/assets/orchestrator.md +8 -12
  6. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +8 -7
  7. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/README.md +10 -0
  8. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/manifest.json +74 -0
  9. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/orchestration/pi.md +53 -0
  10. package/contracts/review-provider-contract-mirror/v1.2.0/bundle/schemas/targeted-validator.schema.json +1 -0
  11. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-capabilities.baseline.json +9 -2
  12. package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/generated/provider-roles.baseline.json +2 -2
  13. package/docs/delegated-verification.md +25 -0
  14. package/docs/review-integration.md +1 -1
  15. package/docs/telemetry.md +38 -0
  16. package/extensions/ask-user-choice.ts +26 -20
  17. package/extensions/codegraph-tools.ts +94 -5
  18. package/extensions/gentle-agents.ts +588 -0
  19. package/extensions/gentle-ai.ts +1421 -143
  20. package/extensions/gentle-shell.ts +547 -0
  21. package/extensions/gentle-todo.ts +199 -0
  22. package/extensions/quiet-tools.ts +1 -1
  23. package/lib/agent-home.ts +8 -0
  24. package/lib/agents-config.ts +318 -0
  25. package/lib/agents-history.ts +80 -0
  26. package/lib/agents-protocol.ts +429 -0
  27. package/lib/agents-runner.ts +490 -0
  28. package/lib/agents-transcript.ts +87 -0
  29. package/lib/agents-view.ts +557 -0
  30. package/lib/agents-widget.ts +222 -0
  31. package/lib/gentle-ai-renderer.ts +142 -26
  32. package/lib/native-choice-list.ts +194 -0
  33. package/lib/native-fullscreen-interaction.ts +47 -0
  34. package/lib/native-pointer-region.ts +164 -0
  35. package/lib/native-review-cli.ts +103 -12
  36. package/lib/provider-contract-bundle.ts +88 -6
  37. package/lib/review-candidate-view-owner.ts +177 -0
  38. package/lib/review-candidate-view.ts +127 -35
  39. package/lib/review-consent-ui.ts +65 -0
  40. package/lib/review-host-relay.ts +146 -60
  41. package/lib/review-integration-v2.ts +92 -13
  42. package/lib/review-last-event-controller.ts +1 -0
  43. package/lib/review-relay-contract.ts +11 -0
  44. package/lib/review-repository.ts +2 -2
  45. package/lib/review-risk-assessment.ts +339 -0
  46. package/lib/review-session-standing-permission-ipc.ts +309 -0
  47. package/lib/review-session-standing-permission.ts +219 -0
  48. package/lib/sdd-preflight.ts +2 -2
  49. package/lib/shell-bar.ts +138 -0
  50. package/lib/shell-card.ts +136 -0
  51. package/lib/shell-changes-view.ts +205 -0
  52. package/lib/shell-changes.ts +210 -0
  53. package/lib/shell-gauge.ts +40 -0
  54. package/lib/shell-prompt.ts +119 -0
  55. package/lib/shell-todo.ts +280 -0
  56. package/lib/shell-usage-view.ts +76 -0
  57. package/lib/shell-usage.ts +246 -0
  58. package/lib/telemetry-trigger.ts +151 -0
  59. package/package.json +4 -4
  60. package/runtime/native-review-cli.mjs +102 -11
  61. package/runtime/review-integration-v2.mjs +92 -13
  62. package/runtime/review-relay-contract.mjs +11 -0
  63. package/runtime/review-risk-assessment.mjs +340 -0
  64. package/runtime/telemetry-trigger.mjs +152 -0
  65. package/scripts/build-runtime-modules.mjs +2 -0
  66. package/scripts/gentle-ai-installer.mjs +10 -10
  67. package/scripts/test-packed-runner.mjs +22 -0
  68. package/scripts/verify-package-files.mjs +18 -13
  69. package/skills/_shared/review-ledger-contract.md +9 -1
  70. package/skills/issue-creation/SKILL.md +53 -93
  71. package/tests/agents-config.test.ts +143 -0
  72. package/tests/agents-fake-child.ts +52 -0
  73. package/tests/agents-history.test.ts +54 -0
  74. package/tests/agents-protocol.test.ts +153 -0
  75. package/tests/agents-runner-process.test.ts +111 -0
  76. package/tests/agents-runner.test.ts +402 -0
  77. package/tests/agents-transcript.test.ts +30 -0
  78. package/tests/agents-view.test.ts +274 -0
  79. package/tests/agents-widget.test.ts +111 -0
  80. package/tests/ask-user-choice.test.ts +157 -3
  81. package/tests/codegraph-tools.test.ts +110 -1
  82. package/tests/devbinary/native-review-parity.devtest.ts +108 -0
  83. package/tests/fixtures/agents-process-child.mjs +23 -0
  84. package/tests/fixtures/provider-contract-bundle/v1.2.0/README.md +22 -0
  85. package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/manifest.json +11 -2
  86. package/tests/fixtures/provider-contract-bundle/v1.2.0/orchestration/pi.md +97 -0
  87. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/lens.schema.json +16 -0
  88. package/tests/fixtures/provider-contract-bundle/v1.2.0/schemas/refuter.schema.json +1 -0
  89. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/lens.json +1 -0
  90. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/refuter.json +1 -0
  91. package/tests/fixtures/provider-contract-bundle/v1.2.0/vectors/targeted-validator.json +1 -0
  92. package/tests/gentle-agents.test.ts +741 -0
  93. package/tests/gentle-ai-binary.test.ts +1 -1
  94. package/tests/gentle-ai-installer.test.ts +47 -47
  95. package/tests/gentle-ai-renderer.test.ts +65 -0
  96. package/tests/gentle-ai.test.ts +31 -14
  97. package/tests/gentle-card-text.ts +35 -0
  98. package/tests/gentle-shell.test.ts +527 -0
  99. package/tests/gentle-todo.test.ts +182 -0
  100. package/tests/issue-creation-skill.test.ts +103 -0
  101. package/tests/native-choice-list.test.ts +202 -0
  102. package/tests/native-fullscreen-interaction.test.ts +125 -0
  103. package/tests/native-pointer-region.test.ts +245 -0
  104. package/tests/native-review-capability-contract.test.ts +33 -1
  105. package/tests/native-review-cli.test.ts +40 -0
  106. package/tests/native-review-consent.test.ts +91 -0
  107. package/tests/native-review-parity-runtime.test.ts +8 -2
  108. package/tests/native-review-parity.test.ts +29 -22
  109. package/tests/orchestrator-budget.test.ts +71 -2
  110. package/tests/orchestrator-rdd-ownership.test.ts +10 -1
  111. package/tests/package-manifest.test.ts +134 -9
  112. package/tests/provider-contract-bundle.test.ts +76 -0
  113. package/tests/provider-contract-mirror.test.ts +19 -0
  114. package/tests/quiet-tool-rendering.test.ts +96 -37
  115. package/tests/rdd-aware-verification-contract.test.ts +216 -0
  116. package/tests/rdd-status-line.test.ts +286 -0
  117. package/tests/review-agent-end-preflight.test.ts +408 -0
  118. package/tests/review-candidate-view.test.ts +452 -6
  119. package/tests/review-contract-prompt.test.ts +142 -0
  120. package/tests/review-controller-native-recovery.test.ts +29 -4
  121. package/tests/review-controller-native-routing.test.ts +321 -4
  122. package/tests/review-controller-workspace-root.test.ts +45 -2
  123. package/tests/review-controller.test.ts +26 -1
  124. package/tests/review-host-relay-routing.test.ts +229 -11
  125. package/tests/review-host-relay.test.ts +195 -7
  126. package/tests/review-integration-v2-forward.test.ts +47 -0
  127. package/tests/review-integration-v2.test.ts +112 -0
  128. package/tests/review-last-event-closure.test.ts +7 -2
  129. package/tests/review-ledger-contract.test.ts +1 -1
  130. package/tests/review-relay-contract.test.ts +26 -0
  131. package/tests/review-repository.test.ts +28 -1
  132. package/tests/review-risk-assessment.test.ts +626 -0
  133. package/tests/review-session-standing-permission-controller.test.ts +608 -0
  134. package/tests/review-session-standing-permission-ipc.test.ts +233 -0
  135. package/tests/review-session-standing-permission-runtime.test.ts +212 -0
  136. package/tests/review-session-standing-permission.test.ts +126 -0
  137. package/tests/runtime-harness.mjs +1 -0
  138. package/tests/shell-bar.test.ts +176 -0
  139. package/tests/shell-card.test.ts +118 -0
  140. package/tests/shell-changes-view.test.ts +146 -0
  141. package/tests/shell-changes.test.ts +182 -0
  142. package/tests/shell-prompt.test.ts +118 -0
  143. package/tests/shell-todo.test.ts +170 -0
  144. package/tests/shell-usage-view.test.ts +62 -0
  145. package/tests/shell-usage.test.ts +197 -0
  146. package/tests/telemetry-trigger.test.ts +349 -0
  147. package/tests/writer-edit-surface-scope.test.ts +153 -17
  148. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/lens.schema.json +0 -0
  149. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/schemas/refuter.schema.json +0 -0
  150. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/lens.json +0 -0
  151. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/refuter.json +0 -0
  152. /package/contracts/review-provider-contract-mirror/{v1.1.0 → v1.2.0}/bundle/vectors/targeted-validator.json +0 -0
  153. /package/{contracts/review-provider-contract-mirror/v1.1.0/bundle → tests/fixtures/provider-contract-bundle/v1.2.0}/schemas/targeted-validator.schema.json +0 -0
@@ -15,3 +15,14 @@
15
15
 
16
16
  export const GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV = "GENTLE_PI_REVIEW_RELAY_CONTRACT";
17
17
  export const GENTLE_PI_REVIEW_RELAY_CONTRACT = "gentle-pi.review-relay/v1";
18
+
19
+ // The host is this extension: every process the session starts (pi's shell
20
+ // tools, Gentle Agents children) runs under it, so the handshake belongs in
21
+ // the session environment too, not only in the extension's own invocations.
22
+ // Without it the first `gentle-ai review status --agent pi` typed in the
23
+ // session shell fails closed (gentle-pi#550). Returns whether anything changed.
24
+ export function declareReviewRelayHandshake(env ) {
25
+ if (env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV] === GENTLE_PI_REVIEW_RELAY_CONTRACT) return false;
26
+ env[GENTLE_PI_REVIEW_RELAY_CONTRACT_ENV] = GENTLE_PI_REVIEW_RELAY_CONTRACT;
27
+ return true;
28
+ }
@@ -0,0 +1,340 @@
1
+ // Generated by scripts/build-runtime-modules.mjs. Do not edit.
2
+ // gentle-pi#662: pure decoding, tier mapping, and verification-plan logic for
3
+ // the native read-only `gentle-ai review assess` command (gentle-ai#4295).
4
+ // Kept independent of the native review CLI wrapper (`lib/native-review-cli.ts`)
5
+ // so the decoder and the verificationPlan table can be unit-tested without a
6
+ // child process, and so the parent tool and the delegation asset agree on one
7
+ // deterministic mapping.
8
+ //
9
+ // Native reference (gentle-ai#4295, landing in parallel with this change):
10
+ // `gentle-ai review assess --cwd <repo> [--base-ref <ref> --committed-only] --json`
11
+ // prints `{"schema":"gentle-ai.review-assessment/v1","risk":"passive|medium|high",
12
+ // "reasons":[{"code":"…","path":"…","detail":"…"}],"changed_paths":1,
13
+ // "changed_lines":1,"candidate":{"kind":"current-changes|base-diff","base_ref":"…"}}`.
14
+ // A non-zero exit or a failure envelope means the candidate could not be
15
+ // assessed; hosts treat that as `high`. Older binaries without the verb (or
16
+ // any other decode/process failure) fail closed the same way -- see
17
+ // `verificationTierForUnassessable` below.
18
+
19
+ export const REVIEW_ASSESSMENT_SCHEMA = "gentle-ai.review-assessment/v1" ;
20
+
21
+ export const REVIEW_ASSESSMENT_RISK = {
22
+ PASSIVE: "passive",
23
+ MEDIUM: "medium",
24
+ HIGH: "high",
25
+ } ;
26
+
27
+
28
+ export const REVIEW_ASSESSMENT_CANDIDATE_KIND = {
29
+ CURRENT_CHANGES: "current-changes",
30
+ BASE_DIFF: "base-diff",
31
+ } ;
32
+
33
+
34
+
35
+
36
+
37
+
38
+
39
+
40
+
41
+
42
+
43
+
44
+
45
+
46
+
47
+
48
+
49
+
50
+
51
+
52
+
53
+
54
+ function isRecord(value ) {
55
+ return typeof value === "object" && value !== null && !Array.isArray(value);
56
+ }
57
+
58
+ function decodeReason(value ) {
59
+ if (!isRecord(value)) throw new TypeError("review assessment reason must be an object");
60
+ const { code, path, detail } = value;
61
+ if (typeof code !== "string" || code.length === 0) throw new TypeError("review assessment reason.code must be a non-empty string");
62
+ if (typeof path !== "string") throw new TypeError("review assessment reason.path must be a string");
63
+ if (typeof detail !== "string") throw new TypeError("review assessment reason.detail must be a string");
64
+ return Object.freeze({ code, path, detail });
65
+ }
66
+
67
+ function decodeCandidate(value ) {
68
+ if (!isRecord(value)) throw new TypeError("review assessment candidate must be an object");
69
+ const { kind, base_ref: baseRef } = value;
70
+ if (kind !== REVIEW_ASSESSMENT_CANDIDATE_KIND.CURRENT_CHANGES && kind !== REVIEW_ASSESSMENT_CANDIDATE_KIND.BASE_DIFF) {
71
+ throw new TypeError("review assessment candidate.kind must be current-changes or base-diff");
72
+ }
73
+ if (baseRef !== undefined && (typeof baseRef !== "string" || baseRef.length === 0)) {
74
+ throw new TypeError("review assessment candidate.base_ref must be a non-empty string when present");
75
+ }
76
+ return Object.freeze({ kind, baseRef: baseRef });
77
+ }
78
+
79
+ /**
80
+ * Decodes and validates one `gentle-ai.review-assessment/v1` envelope. Rejects
81
+ * any other schema, an unrecognized risk value, or a malformed shape -- the
82
+ * native review CLI wrapper turns a thrown error here into the same
83
+ * schema-incompatible failure it already produces for every other decoded
84
+ * native response.
85
+ */
86
+ export function decodeReviewAssessmentV1(value ) {
87
+ if (!isRecord(value)) throw new TypeError("review assessment must be an object");
88
+ if (value.schema !== REVIEW_ASSESSMENT_SCHEMA) throw new TypeError(`review assessment schema must be ${REVIEW_ASSESSMENT_SCHEMA}`);
89
+ const { risk, reasons, changed_paths: changedPaths, changed_lines: changedLines, candidate } = value;
90
+ if (risk !== REVIEW_ASSESSMENT_RISK.PASSIVE && risk !== REVIEW_ASSESSMENT_RISK.MEDIUM && risk !== REVIEW_ASSESSMENT_RISK.HIGH) {
91
+ throw new TypeError("review assessment risk must be passive, medium, or high");
92
+ }
93
+ if (!Array.isArray(reasons)) throw new TypeError("review assessment reasons must be an array");
94
+ if (!Number.isSafeInteger(changedPaths) || (changedPaths ) < 0) throw new TypeError("review assessment changed_paths must be a non-negative integer");
95
+ if (!Number.isSafeInteger(changedLines) || (changedLines ) < 0) throw new TypeError("review assessment changed_lines must be a non-negative integer");
96
+ return Object.freeze({
97
+ schema: REVIEW_ASSESSMENT_SCHEMA,
98
+ risk,
99
+ reasons: Object.freeze(reasons.map(decodeReason)),
100
+ changedPaths: changedPaths ,
101
+ changedLines: changedLines ,
102
+ candidate: decodeCandidate(candidate),
103
+ });
104
+ }
105
+
106
+ // ---------------------------------------------------------------------------
107
+ // Verification-plan tiering (gentle-pi#662).
108
+ // ---------------------------------------------------------------------------
109
+
110
+ export const VERIFICATION_TIER = {
111
+ PASSIVE: "passive",
112
+ MEDIUM: "medium",
113
+ HIGH: "high",
114
+ UNASSESSABLE: "unassessable",
115
+ } ;
116
+
117
+
118
+ export const RDD_LINE = {
119
+ ON: "on",
120
+ OFF: "off",
121
+ UNKNOWN: "unknown",
122
+ } ;
123
+
124
+
125
+ export const WRITER_PROFILE = {
126
+ SMALL: "small",
127
+ LARGE: "large",
128
+ } ;
129
+
130
+
131
+ // gentle-pi#668: whether the native review actually reached a terminal
132
+ // outcome for the current candidate. The `on` branch of `verificationPlan`
133
+ // (the writer self-verifies, the native review is the independent check, no
134
+ // separate verifier) holds only for `CLOSED` -- a human decline for this
135
+ // candidate, a clone-local RDD disable, a refused START/STATUS, or any other
136
+ // non-terminal outcome falls back to the exact same risk-gated path as `off`.
137
+ // `UNKNOWN` is the fail-closed default for an omitted outcome: never treated
138
+ // as `CLOSED`.
139
+ export const NATIVE_REVIEW_OUTCOME = {
140
+ CLOSED: "closed",
141
+ DECLINED: "declined",
142
+ UNAVAILABLE: "unavailable",
143
+ UNKNOWN: "unknown",
144
+ } ;
145
+
146
+
147
+
148
+
149
+
150
+
151
+
152
+
153
+
154
+
155
+
156
+
157
+
158
+
159
+
160
+
161
+
162
+
163
+
164
+
165
+
166
+ /**
167
+ * Builds the risk-gated verification plan shared by the `off`/`unknown` RDD
168
+ * lines and by an `on` line whose native review did not close for this
169
+ * candidate (gentle-pi#668). `gatePrefix` supplies the sentence fragment
170
+ * naming which branch fired, up to and including the word "and"; the
171
+ * tier-specific clause is appended after it.
172
+ */
173
+ function riskGatedPlan(gatePrefix , risk , writerProfile ) {
174
+ if (risk === VERIFICATION_TIER.PASSIVE) {
175
+ return Object.freeze({
176
+ writerSelfVerification: false,
177
+ structuralReadbackOnly: true,
178
+ independentVerifier: false,
179
+ reason: `${gatePrefix} the native assessment reports passive risk: a structural readback by the parent is the complete check, with no separate verifier and no tests.`,
180
+ });
181
+ }
182
+
183
+ if (risk === VERIFICATION_TIER.MEDIUM) {
184
+ const smallModelBias = writerProfile === WRITER_PROFILE.SMALL;
185
+ return Object.freeze({
186
+ writerSelfVerification: true,
187
+ structuralReadbackOnly: false,
188
+ independentVerifier: smallModelBias,
189
+ reason: smallModelBias
190
+ ? `${gatePrefix} the native assessment reports medium risk, but the small-model bias raises it one tier to high for verification purposes: the writer self-verifies and a separate independent verifier also runs.`
191
+ : `${gatePrefix} the native assessment reports medium risk with a large writer profile: the writer's self-verification stands and no separate verifier is required.`,
192
+ });
193
+ }
194
+
195
+ // high or unassessable.
196
+ return Object.freeze({
197
+ writerSelfVerification: true,
198
+ structuralReadbackOnly: false,
199
+ independentVerifier: true,
200
+ reason: risk === VERIFICATION_TIER.UNASSESSABLE
201
+ ? `${gatePrefix} the native risk assessment could not be produced, so the candidate is treated as high risk: the writer self-verifies and a separate independent verifier always runs.`
202
+ : `${gatePrefix} the native assessment reports high risk: the writer self-verifies and a separate independent verifier always runs.`,
203
+ });
204
+ }
205
+
206
+ // Names why an `on` line falls back to the risk-gated path, for every
207
+ // non-closed native review outcome (gentle-pi#668). `UNKNOWN` covers both an
208
+ // omitted outcome and a caller-supplied one this decoder does not
209
+ // recognize -- both fail closed the same way.
210
+ function nonClosedOutcomeClause(outcome ) {
211
+ switch (outcome) {
212
+ case NATIVE_REVIEW_OUTCOME.DECLINED:
213
+ return "the native review was declined for this candidate";
214
+ case NATIVE_REVIEW_OUTCOME.UNAVAILABLE:
215
+ return "the native review is unavailable for this candidate";
216
+ default:
217
+ return "the native review outcome for this candidate is unknown";
218
+ }
219
+ }
220
+
221
+ /**
222
+ * Computes who verifies a delegated writer's change, exactly as
223
+ * gentle-pi#662/#668 specify:
224
+ *
225
+ * - `rdd: "on"` AND the native review reached `NATIVE_REVIEW_OUTCOME.CLOSED`
226
+ * for this candidate: the writer self-verifies and no independent verifier
227
+ * runs, because the closed native review is the check -- except `passive`,
228
+ * which gets a structural readback by the parent instead.
229
+ * - `rdd: "on"` with any other outcome (`declined`, `unavailable`, or the
230
+ * fail-closed `unknown` default for an omitted/unrecognized outcome): the
231
+ * `on` branch never held for this candidate, so the risk-gated path below
232
+ * applies exactly as `off` -- declining a review is candidate-scoped and
233
+ * never lowers the bar below the RDD-off path.
234
+ * - `rdd: "off"` or `"unknown"` (both gate identically, so an unknown RDD line
235
+ * never lowers a tier relative to `off`; `nativeReviewOutcome` is ignored
236
+ * entirely on these lines):
237
+ * - `passive`: structural readback by the parent only; no verifier, no
238
+ * tests.
239
+ * - `medium`: the writer self-verifies; a separate independent verifier
240
+ * runs only when `writerProfile` is `"small"` -- the small-model bias
241
+ * raises the tier by one for verification purposes (medium -> high). An
242
+ * unknown or omitted writer profile resolves to `"small"` before it
243
+ * reaches this function (see `resolveWriterProfile`), so an unrecognized
244
+ * profile never lowers a tier below what a known small model would get.
245
+ * - `high` or `unassessable` (a failed or unrecognized native assessment is
246
+ * always treated as high): the writer self-verifies and an independent
247
+ * verifier always runs.
248
+ */
249
+ export function verificationPlan(input ) {
250
+ const { rddLine, risk, writerProfile } = input;
251
+
252
+ if (rddLine === RDD_LINE.ON) {
253
+ const outcome = input.nativeReviewOutcome ?? NATIVE_REVIEW_OUTCOME.UNKNOWN;
254
+ if (outcome === NATIVE_REVIEW_OUTCOME.CLOSED) {
255
+ if (risk === VERIFICATION_TIER.PASSIVE) {
256
+ return Object.freeze({
257
+ writerSelfVerification: false,
258
+ structuralReadbackOnly: true,
259
+ independentVerifier: false,
260
+ reason: "receipt-driven development is on and the native review closed for this candidate; the assessment also reports passive risk: a structural readback by the parent replaces both the writer's self-verification and any separate verifier.",
261
+ });
262
+ }
263
+ return Object.freeze({
264
+ writerSelfVerification: true,
265
+ structuralReadbackOnly: false,
266
+ independentVerifier: false,
267
+ reason: "receipt-driven development is on and the native review closed for this candidate: the writer's self-verification is the record and the closed native review was the independent check the writer cannot influence, so no separate verifier is required.",
268
+ });
269
+ }
270
+ // The on branch holds only while the native review reaches a terminal
271
+ // outcome for this candidate. Anything else -- declined, unavailable, or
272
+ // unknown -- falls back to the exact same risk-gated path as off.
273
+ return riskGatedPlan(
274
+ `receipt-driven development is on, but ${nonClosedOutcomeClause(outcome)}, so the risk-gated path applies exactly as receipt-driven development off, and`,
275
+ risk,
276
+ writerProfile,
277
+ );
278
+ }
279
+
280
+ // rddLine is "off" or "unknown" here -- both gate on the native risk
281
+ // assessment identically, so an unknown RDD line never lowers a tier, and
282
+ // neither line ever consults nativeReviewOutcome.
283
+ return riskGatedPlan(`receipt-driven development is ${rddLine} and`, risk, writerProfile);
284
+ }
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // Small-writer-profile predicate (gentle-pi#662).
288
+ // ---------------------------------------------------------------------------
289
+
290
+ /**
291
+ * The minimal shape of `lib/agents-config.ts`'s `ResolvedProfile` this
292
+ * predicate needs: the resolved model reference and its resolved effort
293
+ * (`ResolvedProfile.thinking`, one of `ThinkingLevel`).
294
+ */
295
+
296
+
297
+
298
+
299
+
300
+ // Matches `mini` only as a whole token of the model id -- delimited by the
301
+ // string start/end or one of `-_./:` or whitespace -- never a bare substring.
302
+ // `gpt-5.4-mini`, `o4-mini`, `openai/gpt-5.4-mini`, `claude-mini`, and
303
+ // `mini-high` all match; `gemini`, `gemini-2.5-pro`, and `gemini-2.5-flash`
304
+ // do not, because their `mini` is preceded by `ge`, not a delimiter.
305
+ const SMALL_MODEL_ID_TOKEN = /(?:^|[-_./:\s])mini(?:$|[-_./:\s])/i;
306
+
307
+ /**
308
+ * A writer profile counts as a "small model" for the medium-risk bias when:
309
+ * - its resolved effort is `low`, or
310
+ * - its resolved model id carries `mini` as a whole token (see
311
+ * `SMALL_MODEL_ID_TOKEN`), or
312
+ * - it carries NO signal at all -- no profile object, or a profile with
313
+ * neither a model id nor a resolved effort. An unknown or omitted writer
314
+ * profile fails closed to small rather than defaulting to large, so a
315
+ * caller that cannot resolve the writer's profile never gets a weaker
316
+ * verification plan than a known small model would (gentle-pi#662).
317
+ *
318
+ * A profile that carries a model id or an effort, just not a matching one
319
+ * (for example `{model: {id: "claude-sonnet-5"}, thinking: "high"}`), is
320
+ * "known large" and returns `false` -- fail-closed applies only to a truly
321
+ * unknown/omitted profile, not to an explicitly resolved large one.
322
+ */
323
+ export function isSmallWriterProfile(profile ) {
324
+ if (profile === undefined) return true;
325
+ if (profile.thinking === "low") return true;
326
+ const modelId = profile.model?.id;
327
+ if (typeof modelId === "string") return SMALL_MODEL_ID_TOKEN.test(modelId);
328
+ // No model id was resolved. Fail closed to small only when effort is also
329
+ // unknown -- there is no signal at all to call this a known large model.
330
+ return profile.thinking === undefined;
331
+ }
332
+
333
+ /**
334
+ * Maps a resolved writer profile to the `writerProfile` input `verificationPlan`
335
+ * expects. An unknown or omitted profile never lowers a tier: it resolves to
336
+ * `"small"`, exactly like `isSmallWriterProfile` (gentle-pi#662).
337
+ */
338
+ export function resolveWriterProfile(profile ) {
339
+ return isSmallWriterProfile(profile) ? WRITER_PROFILE.SMALL : WRITER_PROFILE.LARGE;
340
+ }
@@ -0,0 +1,152 @@
1
+ // Generated by scripts/build-runtime-modules.mjs. Do not edit.
2
+ import { spawn as nodeSpawn } from "node:child_process";
3
+
4
+ // gentle-ai#4309 owns anonymous usage telemetry end to end: install and
5
+ // heartbeat events, the notice, every kill switch, and the wire contract.
6
+ // gentle-pi#677 only nudges the local binary once per process, best-effort,
7
+ // and never surfaces the outcome to the user. This module is the pure,
8
+ // testable core of that nudge (`shouldTriggerTelemetry`,
9
+ // `spawnTelemetryTrigger`) plus the decode helper the foreground
10
+ // `/gentle:telemetry` slash command uses to relay `<op> --json` output
11
+ // (`decodeTelemetryTriggerDecision`).
12
+
13
+ export const TELEMETRY_TRIGGER_KILL_TIMEOUT_MS = 3_000;
14
+
15
+ export const TELEMETRY_TRIGGER_CONTRACT = "gentle-ai.telemetry-trigger/v1";
16
+
17
+ export const TELEMETRY_TRIGGER_DECISIONS = [
18
+ "enrolled",
19
+ "sent_install",
20
+ "sent_heartbeat",
21
+ "rate_limited",
22
+ "backoff",
23
+ "disabled",
24
+ ] ;
25
+
26
+
27
+
28
+
29
+
30
+
31
+
32
+
33
+ /**
34
+ * Decodes `gentle-ai telemetry trigger --json` stdout. An older binary
35
+ * without the verb prints `unknown telemetry command` and exits non-zero, so
36
+ * its stdout never parses as JSON here; that -- along with malformed JSON, a
37
+ * mismatched schema, an unrecognized decision, or a missing/non-string
38
+ * source -- means "nothing to do" and returns undefined rather than
39
+ * throwing.
40
+ */
41
+ export function decodeTelemetryTriggerDecision(stdout ) {
42
+ let parsed ;
43
+ try {
44
+ parsed = JSON.parse(stdout);
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return undefined;
49
+ const record = parsed ;
50
+ if (record.schema !== TELEMETRY_TRIGGER_CONTRACT) return undefined;
51
+ if (typeof record.decision !== "string") return undefined;
52
+ if (!(TELEMETRY_TRIGGER_DECISIONS ).includes(record.decision)) return undefined;
53
+ if (typeof record.source !== "string" || record.source.length === 0) return undefined;
54
+ return {
55
+ schema: TELEMETRY_TRIGGER_CONTRACT,
56
+ decision: record.decision ,
57
+ source: record.source,
58
+ };
59
+ }
60
+
61
+
62
+
63
+ function telemetryKillSwitchReason(env ) {
64
+ if (env.DO_NOT_TRACK === "1") return "do-not-track";
65
+ if (env.GENTLE_AI_TELEMETRY === "0") return "telemetry-disabled";
66
+ if (env.CI === "true") return "ci";
67
+ return undefined;
68
+ }
69
+
70
+ /**
71
+ * True unless one of Gentle Pi's own kill switches is set
72
+ * (`DO_NOT_TRACK=1`, `GENTLE_AI_TELEMETRY=0`, or `CI=true`). The binary
73
+ * enforces its own opt-out and rate limiting independently; this predicate
74
+ * only decides whether Gentle Pi spawns the trigger at all.
75
+ */
76
+ export function shouldTriggerTelemetry(env ) {
77
+ return telemetryKillSwitchReason(env) === undefined;
78
+ }
79
+
80
+ // A minimal structural subset of node:child_process's ChildProcess, mirroring
81
+ // the ChildLike/Spawn seam already used by lib/agents-runner.ts: narrow
82
+ // enough that tests can stub it without touching a real process.
83
+
84
+
85
+
86
+
87
+
88
+
89
+
90
+
91
+
92
+
93
+
94
+
95
+
96
+
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+
106
+
107
+
108
+
109
+
110
+
111
+
112
+
113
+
114
+
115
+
116
+ /**
117
+ * Nudges `<executable> telemetry trigger --json`, detached and
118
+ * fire-and-forget. Never throws, never awaits the child's exit, and never
119
+ * blocks session start: a missing binary, an old binary without the verb, or
120
+ * a spawn failure all just report `{ spawned: false, ... }`. A 3 s timer
121
+ * kills a runaway child; the timer is unref'd so it can never keep the host
122
+ * process alive on its own, matching the detached child's own `unref()`.
123
+ */
124
+ export function spawnTelemetryTrigger(options ) {
125
+ const killSwitch = telemetryKillSwitchReason(options.env);
126
+ if (killSwitch !== undefined) return { spawned: false, reason: killSwitch };
127
+ const spawnImpl = options.spawn ?? (nodeSpawn );
128
+ try {
129
+ const child = spawnImpl(options.executable, ["telemetry", "trigger", "--json"], {
130
+ cwd: options.cwd,
131
+ env: options.env,
132
+ detached: true,
133
+ stdio: "ignore",
134
+ });
135
+ child.on("error", () => {
136
+ // Fire-and-forget: a spawn-time or runtime child error never surfaces
137
+ // to the caller and never affects session start.
138
+ });
139
+ const killTimer = setTimeout(() => {
140
+ try {
141
+ child.kill();
142
+ } catch {
143
+ // Best effort only; the child may already have exited.
144
+ }
145
+ }, TELEMETRY_TRIGGER_KILL_TIMEOUT_MS);
146
+ killTimer.unref?.();
147
+ child.unref();
148
+ return { spawned: true, reason: "spawned" };
149
+ } catch {
150
+ return { spawned: false, reason: "spawn-error" };
151
+ }
152
+ }
@@ -10,7 +10,9 @@ const sources = [
10
10
  "gentle-ai-binary",
11
11
  "review-relay-contract",
12
12
  "review-integration-v2",
13
+ "review-risk-assessment",
13
14
  "native-review-cli",
15
+ "telemetry-trigger",
14
16
  ];
15
17
  const header = "// Generated by scripts/build-runtime-modules.mjs. Do not edit.\n";
16
18
 
@@ -36,7 +36,7 @@ const WINDOWS_SYSTEM_ROOT = "C:\\Windows";
36
36
  // version check below) derives from this constant instead of repeating the
37
37
  // literal, so a pin bump cannot leave a stale copy behind. See
38
38
  // scripts/install-gentle-ai.mjs for the incident that motivated this.
39
- export const INSTALLER_VERSION = "2.5.0";
39
+ export const INSTALLER_VERSION = "2.7.0";
40
40
  export const RELEASE_BASE_URL = `https://github.com/Gentleman-Programming/gentle-ai/releases/download/v${INSTALLER_VERSION}/`;
41
41
  export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
42
42
  SIGNED_RELEASE_ASSET: "signed-release-asset",
@@ -45,10 +45,10 @@ export const GENTLE_AI_INSTALL_METHOD = Object.freeze({
45
45
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH = "github.com/gentleman-programming/gentle-ai/v2/cmd/gentle-ai";
46
46
  export const GENTLE_AI_WINDOWS_SOURCE_MODULE = "github.com/gentleman-programming/gentle-ai/v2";
47
47
  export const GENTLE_AI_WINDOWS_SOURCE_TAG = `v${INSTALLER_VERSION}`;
48
- // `go mod download -json github.com/gentleman-programming/gentle-ai/v2@v2.5.0`
48
+ // `go mod download -json github.com/gentleman-programming/gentle-ai/v2@v2.7.0`
49
49
  // with GOSUMDB=sum.golang.org reports this exact module SumDB checksum, and the
50
- // tag resolves to commit f5dd1a6c, the published v2.5.0 release head.
51
- export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:0nvTLJFAf9ruBDwTvi+aDrWt/dt/GKvUP2cFVB6c2DI=";
50
+ // tag resolves to commit 4e4470b3, the published v2.7.0 release head.
51
+ export const GENTLE_AI_WINDOWS_SOURCE_MODULE_CHECKSUM = "h1:SE4KCLo3y1qWaTq+7HJKZ1DkAAAl+w+82YY0+qQkFwM=";
52
52
  export const GENTLE_AI_WINDOWS_SOURCE_PACKAGE = `${GENTLE_AI_WINDOWS_SOURCE_PACKAGE_PATH}@${GENTLE_AI_WINDOWS_SOURCE_TAG}`;
53
53
  export const GENTLE_AI_WINDOWS_MINIMUM_GO_VERSION = "1.25.10";
54
54
  export const GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE_CODE = "GENTLE_AI_GO_TOOLCHAIN_UNAVAILABLE";
@@ -67,7 +67,7 @@ export class GentleAiInstallerError extends Error {
67
67
  // Sentinel used while a re-pinned gentle-ai release is not yet published. A
68
68
  // sentinel digest can never match a real SHA-256, so installation fails closed,
69
69
  // and verify-package-files.mjs refuses to pack/publish while any digest below
70
- // still holds it. The v2.5.0 digests are pinned from the published release:
70
+ // still holds it. The v2.7.0 digests are pinned from the published release:
71
71
  // archive sha256 values verified against the minisign-signed checksums.txt and
72
72
  // freshly computed hashes; binary sha256 values computed from the extracted
73
73
  // executables.
@@ -109,15 +109,15 @@ async function downloadPinnedGentleAiAsset(asset, destination, options) {
109
109
  }
110
110
 
111
111
  // Windows is absent from signed release archives on purpose. gentle-ai stopped
112
- // distributing unsigned Windows builds in c4b764d0, so v2.5.0 publishes signed
112
+ // distributing unsigned Windows builds in c4b764d0, so v2.7.0 publishes signed
113
113
  // Darwin/Linux archives only. Windows x64/arm64 uses the separately verified
114
114
  // exact-tag Go SumDB source-build path below; restore archive rows only when
115
115
  // upstream ships signed Windows assets.
116
116
  export const GENTLE_AI_RELEASE_ASSETS = Object.freeze({
117
- "darwin/amd64": asset("gentle-ai_2.5.0_darwin_amd64.tar.gz", "8204ed692d7ff16fae2eb8bd7b3132b5e4e4dd7b9ea40e93bfced57394da27c0", "929817d320e94ad62be7ed546ad8746e62bd63ab97e3f38c94456f221f163eca", "gentle-ai"),
118
- "darwin/arm64": asset("gentle-ai_2.5.0_darwin_arm64.tar.gz", "ce06831cea9fd2ac7b2b36d9e98e63988246e0fbafbbd2cc7c003ec2f1463a23", "c859cd88138eb41e83ef9b75309697aaff2acb9f720c9a029b432095f66aea02", "gentle-ai"),
119
- "linux/amd64": asset("gentle-ai_2.5.0_linux_amd64.tar.gz", "2ba84a3a7ba2b1193019bde2acb05b02cbf222b667568c919686352b3caab113", "5acedd14b7927a7665636bf728c14a614597ce85dd91313bc50cbc96881c6cab", "gentle-ai"),
120
- "linux/arm64": asset("gentle-ai_2.5.0_linux_arm64.tar.gz", "16a6243d17c146e3fc0024f6f005c5bd141fbb2dda4b56e269318e499ddc170b", "ce75ba1174955813ef462f4e7a50dd59a8e8167575033f6e17aa8d8488630cb8", "gentle-ai"),
117
+ "darwin/amd64": asset("gentle-ai_2.7.0_darwin_amd64.tar.gz", "5edf1e38894d6b2c1d257583cf4da23e2471812ed2efd0de33cbeef608339609", "e7aa77701e3cfd8a382a756ff2f257ba1a94ab4699050b3c3ac8e6237eb53950", "gentle-ai"),
118
+ "darwin/arm64": asset("gentle-ai_2.7.0_darwin_arm64.tar.gz", "a9efa8cf182d853b7d0115efbc73a83465d435ec429f6465bea9181ea8022fde", "497932a73664a958cf46b0b131277da4a41a48f8d7da4f777d1157f8ffa9f646", "gentle-ai"),
119
+ "linux/amd64": asset("gentle-ai_2.7.0_linux_amd64.tar.gz", "ff765a183247fe31d4739ff6df3d6874740979987ea310de3856c8a6e85375e9", "8748b9f18c05a831692abc0ae560fdc50a4e4c8f492705edcce7f65e067a7b12", "gentle-ai"),
120
+ "linux/arm64": asset("gentle-ai_2.7.0_linux_arm64.tar.gz", "d8e114cd4d226f6c974813a49fce4d3e2028f4880b87e08f270eadf0a0607561", "65dd6bd7249d5eb7a2c948eebd0aee1bf2e85e1a2123e9b3782e978b167c3875", "gentle-ai"),
121
121
  });
122
122
 
123
123
  // A pinned asset is either a signed archive or, for a prerelease pin only,
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import assert from "node:assert/strict";
3
4
  import { execFileSync } from "node:child_process";
4
5
  import { createHash } from "node:crypto";
5
6
  import { existsSync, mkdtempSync, mkdirSync, readFileSync, readdirSync, realpathSync, rmSync, writeFileSync } from "node:fs";
@@ -50,6 +51,27 @@ try {
50
51
  stdio: "inherit",
51
52
  });
52
53
  const packageRoot = join(installDirectory, "node_modules", "gentle-pi");
54
+ const { nativeReviewAbandonAuthorization } = await import(pathToFileURL(join(packageRoot, "runtime", "native-review-cli.mjs")).href);
55
+ const abandonAuthorization = nativeReviewAbandonAuthorization({
56
+ lineage: "review-abc",
57
+ expectedRevision: "revision-9",
58
+ snapshotIdentity: "snapshot-1",
59
+ capturedLensResults: ["00-risk.json", "01-refuter.json"],
60
+ findingsPresent: true,
61
+ actor: "maintainer",
62
+ reason: "operator_disposition",
63
+ });
64
+ assert.equal(abandonAuthorization, [
65
+ "gentle-ai.review-abandon-authorization/v2",
66
+ "lineage=review-abc",
67
+ "revision=revision-9",
68
+ "snapshot_identity=snapshot-1",
69
+ "reason=operator_disposition",
70
+ "captured_lens_results=00-risk.json,01-refuter.json",
71
+ "findings_present=true",
72
+ "actor=maintainer",
73
+ ].join("\n"));
74
+ assert.ok(!abandonAuthorization.includes("evidence_records_present"));
53
75
  // Accept prerelease pins too: a stable-only pattern here was a second,
54
76
  // silent pin that refused the first prerelease version directory.
55
77
  const versions = readdirSync(join(packageRoot, ".gentle-ai"), { withFileTypes: true }).filter((entry) => entry.isDirectory() && /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z][0-9A-Za-z.]*)?$/.test(entry.name));