gentle-pi 2.4.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 (227) hide show
  1. package/README.md +292 -25
  2. package/assets/agents/gentle-ai-worker.md +13 -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 +39 -11
  25. package/assets/orchestrator.md +5 -5
  26. package/assets/sdd-orchestrator-workflow.md +54 -21
  27. package/assets/support/sdd-status-contract.md +34 -90
  28. package/contracts/telemetry/runtime-aggregate-v1.schema.json +65 -0
  29. package/docs/delegated-verification.md +25 -0
  30. package/docs/telemetry.md +94 -0
  31. package/docs/windows-startup-console-visibility.md +18 -0
  32. package/extensions/ask-user-choice.ts +159 -25
  33. package/extensions/codegraph-tools.ts +95 -5
  34. package/extensions/gentle-agents.ts +1337 -0
  35. package/extensions/gentle-ai.ts +2916 -386
  36. package/extensions/gentle-shell.ts +650 -0
  37. package/extensions/gentle-todo.ts +234 -0
  38. package/extensions/quiet-tools.ts +2 -1
  39. package/extensions/runtime-metrics.ts +130 -0
  40. package/extensions/sdd-init.ts +2 -2
  41. package/extensions/startup-banner.ts +52 -75
  42. package/lib/agent-profiles.ts +550 -0
  43. package/lib/agents-completion-delivery.ts +72 -0
  44. package/lib/agents-config.ts +315 -0
  45. package/lib/agents-history.ts +88 -0
  46. package/lib/agents-messaging.ts +187 -0
  47. package/lib/agents-protocol.ts +501 -0
  48. package/lib/agents-runner.ts +1012 -0
  49. package/lib/agents-thread-view.ts +57 -0
  50. package/lib/agents-transcript.ts +87 -0
  51. package/lib/agents-view-layout.ts +40 -0
  52. package/lib/agents-view.ts +914 -0
  53. package/lib/agents-widget.ts +241 -0
  54. package/lib/gentle-ai-binary.ts +3 -1
  55. package/lib/gentle-ai-renderer.ts +143 -25
  56. package/lib/native-choice-list.ts +194 -0
  57. package/lib/native-fullscreen-interaction.ts +47 -0
  58. package/lib/native-pointer-region.ts +164 -0
  59. package/lib/native-review-cli.ts +371 -13
  60. package/lib/orchestrator-presence.ts +337 -0
  61. package/lib/profiles-orchestrator.ts +203 -0
  62. package/lib/review-candidate-view-owner.ts +427 -0
  63. package/lib/review-candidate-view.ts +150 -48
  64. package/lib/review-consent-component.ts +247 -0
  65. package/lib/review-consent-ui.ts +110 -0
  66. package/lib/review-host-relay.ts +28 -0
  67. package/lib/review-integration-v2.ts +243 -11
  68. package/lib/review-last-event-controller.ts +8 -4
  69. package/lib/review-relay-contract.ts +11 -0
  70. package/lib/review-reminder-receipt.ts +74 -0
  71. package/lib/review-repository.ts +2 -2
  72. package/lib/review-risk-assessment.ts +339 -0
  73. package/lib/review-session-standing-permission-ipc.ts +309 -0
  74. package/lib/review-session-standing-permission.ts +240 -0
  75. package/lib/runtime-metrics-children.ts +199 -0
  76. package/lib/runtime-metrics-delivery.ts +68 -0
  77. package/lib/runtime-metrics-native.ts +166 -0
  78. package/lib/runtime-metrics-pi-identity.ts +113 -0
  79. package/lib/runtime-metrics-policy.ts +51 -0
  80. package/lib/runtime-metrics.ts +255 -0
  81. package/lib/sdd-preflight.ts +362 -81
  82. package/lib/sdd-research-capabilities.ts +228 -0
  83. package/lib/sdd-status.ts +29 -7
  84. package/lib/session-worktree-registry.ts +118 -0
  85. package/lib/shell-bar.ts +184 -0
  86. package/lib/shell-card.ts +133 -0
  87. package/lib/shell-changes-view.ts +530 -0
  88. package/lib/shell-changes.ts +290 -0
  89. package/lib/shell-gauge.ts +40 -0
  90. package/lib/shell-prompt.ts +115 -0
  91. package/lib/shell-sidebar-banner.ts +11 -0
  92. package/lib/shell-sidebar-layout.ts +213 -0
  93. package/lib/shell-sidebar.ts +41 -0
  94. package/lib/shell-todo.ts +297 -0
  95. package/lib/shell-usage-view.ts +76 -0
  96. package/lib/shell-usage.ts +246 -0
  97. package/lib/telemetry-trigger.ts +153 -0
  98. package/package.json +8 -5
  99. package/runtime/gentle-ai-binary.mjs +3 -1
  100. package/runtime/native-review-cli.mjs +370 -12
  101. package/runtime/review-integration-v2.mjs +243 -11
  102. package/runtime/review-relay-contract.mjs +11 -0
  103. package/runtime/review-risk-assessment.mjs +340 -0
  104. package/runtime/telemetry-trigger.mjs +154 -0
  105. package/scripts/build-runtime-modules.mjs +11 -1
  106. package/scripts/check-types.mjs +125 -0
  107. package/scripts/gentle-ai-installer.mjs +10 -10
  108. package/scripts/install-gentle-ai.mjs +12 -0
  109. package/scripts/install-tui-mode-setting.mjs +114 -0
  110. package/scripts/test-packed-runner.mjs +38 -2
  111. package/scripts/types-baseline.json +99 -0
  112. package/scripts/verify-package-files.mjs +8 -2
  113. package/skills/_shared/review-ledger-contract.md +20 -2
  114. package/skills/issue-creation/SKILL.md +3 -3
  115. package/skills/judgment-day/SKILL.md +17 -3
  116. package/skills/judgment-day/references/prompts-and-formats.md +14 -3
  117. package/tests/agent-profiles.test.ts +722 -0
  118. package/tests/agents-completion-delivery.test.ts +94 -0
  119. package/tests/agents-config.test.ts +205 -0
  120. package/tests/agents-fake-child.ts +66 -0
  121. package/tests/agents-grouping.test.ts +179 -0
  122. package/tests/agents-history.test.ts +54 -0
  123. package/tests/agents-integration.test.ts +100 -0
  124. package/tests/agents-messaging.test.ts +94 -0
  125. package/tests/agents-protocol.test.ts +198 -0
  126. package/tests/agents-queries.test.ts +190 -0
  127. package/tests/agents-responsive.test.ts +43 -0
  128. package/tests/agents-runner-process.test.ts +111 -0
  129. package/tests/agents-runner.test.ts +959 -0
  130. package/tests/agents-thread-view.test.ts +45 -0
  131. package/tests/agents-transcript.test.ts +30 -0
  132. package/tests/agents-view.test.ts +685 -0
  133. package/tests/agents-widget.test.ts +141 -0
  134. package/tests/artifact-language.test.ts +25 -2
  135. package/tests/ask-user-choice.test.ts +325 -5
  136. package/tests/asset-installation-runtime.test.ts +108 -0
  137. package/tests/autonomous-guard.test.ts +116 -1
  138. package/tests/codegraph-tools.test.ts +112 -2
  139. package/tests/delegated-key-learnings-contract.test.ts +1 -1
  140. package/tests/devbinary/native-review-parity.devtest.ts +110 -0
  141. package/tests/feature-request-form.test.ts +67 -0
  142. package/tests/fixtures/agents-messaging-child.mjs +5 -0
  143. package/tests/fixtures/agents-process-child.mjs +23 -0
  144. package/tests/fixtures/runtime-metrics-native-batches.json +6 -0
  145. package/tests/gentle-agents.test.ts +2168 -0
  146. package/tests/gentle-ai-binary.test.ts +7 -2
  147. package/tests/gentle-ai-installer.test.ts +47 -47
  148. package/tests/gentle-ai-renderer.test.ts +103 -0
  149. package/tests/gentle-ai.test.ts +971 -15
  150. package/tests/gentle-card-text.ts +35 -0
  151. package/tests/gentle-shell.test.ts +818 -0
  152. package/tests/gentle-todo.test.ts +226 -0
  153. package/tests/install-tui-mode-setting.test.ts +324 -0
  154. package/tests/issue-creation-skill.test.ts +22 -0
  155. package/tests/model-routing-authority.test.ts +12 -0
  156. package/tests/native-choice-list.test.ts +202 -0
  157. package/tests/native-fullscreen-interaction.test.ts +125 -0
  158. package/tests/native-pointer-region.test.ts +245 -0
  159. package/tests/native-review-capability-contract.test.ts +27 -1
  160. package/tests/native-review-cli.test.ts +317 -3
  161. package/tests/native-review-consent.test.ts +91 -0
  162. package/tests/native-review-parity-runtime.test.ts +8 -2
  163. package/tests/native-review-parity.test.ts +43 -29
  164. package/tests/native-sdd-attempt-authority.test.ts +7 -2
  165. package/tests/orchestrator-budget.test.ts +69 -0
  166. package/tests/orchestrator-presence.test.ts +389 -0
  167. package/tests/orchestrator-rdd-ownership.test.ts +9 -0
  168. package/tests/package-manifest.test.ts +243 -7
  169. package/tests/profiles-orchestrator.test.ts +208 -0
  170. package/tests/quiet-tool-rendering.test.ts +97 -37
  171. package/tests/rdd-aware-verification-contract.test.ts +226 -0
  172. package/tests/rdd-status-line.test.ts +286 -0
  173. package/tests/review-agent-end-preflight.test.ts +332 -24
  174. package/tests/review-candidate-view.test.ts +751 -7
  175. package/tests/review-consent-ui.test.ts +352 -0
  176. package/tests/review-contract-prompt.test.ts +17 -0
  177. package/tests/review-controller-native-recovery.test.ts +29 -4
  178. package/tests/review-controller-native-routing.test.ts +884 -7
  179. package/tests/review-controller-workspace-root.test.ts +45 -2
  180. package/tests/review-controller.test.ts +26 -1
  181. package/tests/review-host-relay-restart-parity.test.ts +142 -1
  182. package/tests/review-host-relay-routing.test.ts +384 -8
  183. package/tests/review-host-relay.test.ts +29 -0
  184. package/tests/review-integration-v2-forward.test.ts +44 -0
  185. package/tests/review-integration-v2.test.ts +276 -0
  186. package/tests/review-last-event-closure.test.ts +112 -3
  187. package/tests/review-ledger-contract.test.ts +61 -6
  188. package/tests/review-relay-contract.test.ts +26 -0
  189. package/tests/review-reminder-receipt.test.ts +62 -0
  190. package/tests/review-repository.test.ts +28 -1
  191. package/tests/review-risk-assessment.test.ts +626 -0
  192. package/tests/review-session-standing-permission-controller.test.ts +656 -0
  193. package/tests/review-session-standing-permission-ipc.test.ts +233 -0
  194. package/tests/review-session-standing-permission-runtime.test.ts +212 -0
  195. package/tests/review-session-standing-permission.test.ts +156 -0
  196. package/tests/runtime-harness.mjs +447 -39
  197. package/tests/runtime-metrics-children.test.ts +206 -0
  198. package/tests/runtime-metrics-delivery.test.ts +85 -0
  199. package/tests/runtime-metrics-extension.test.ts +187 -0
  200. package/tests/runtime-metrics-native.test.ts +209 -0
  201. package/tests/runtime-metrics-pi-identity.test.ts +113 -0
  202. package/tests/runtime-metrics-policy.test.ts +62 -0
  203. package/tests/runtime-metrics.test.ts +184 -0
  204. package/tests/sdd-agent-tools.test.ts +10 -1
  205. package/tests/sdd-execution-routing-contract.test.ts +28 -0
  206. package/tests/sdd-managed-runtime-settlement.test.ts +331 -0
  207. package/tests/sdd-native-managed-uptake.test.ts +253 -0
  208. package/tests/sdd-planning-routing-contract.test.ts +45 -0
  209. package/tests/sdd-preflight.test.ts +252 -8
  210. package/tests/sdd-research-capabilities.test.ts +256 -0
  211. package/tests/sdd-research-live.test.ts +241 -0
  212. package/tests/sdd-selection-transport.test.ts +504 -0
  213. package/tests/sdd-status.test.ts +51 -0
  214. package/tests/session-worktree-registry.test.ts +135 -0
  215. package/tests/shell-bar.test.ts +176 -0
  216. package/tests/shell-card.test.ts +139 -0
  217. package/tests/shell-changes-view.test.ts +609 -0
  218. package/tests/shell-changes.test.ts +350 -0
  219. package/tests/shell-prompt.test.ts +140 -0
  220. package/tests/shell-sidebar-banner.test.ts +23 -0
  221. package/tests/shell-sidebar-layout.test.ts +387 -0
  222. package/tests/shell-sidebar.test.ts +50 -0
  223. package/tests/shell-todo.test.ts +259 -0
  224. package/tests/shell-usage-view.test.ts +62 -0
  225. package/tests/shell-usage.test.ts +197 -0
  226. package/tests/startup-banner.test.ts +126 -0
  227. package/tests/telemetry-trigger.test.ts +351 -0
@@ -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,154 @@
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
+ /**
118
+ * Nudges `<executable> telemetry trigger --json`, detached and
119
+ * fire-and-forget. Never throws, never awaits the child's exit, and never
120
+ * blocks session start: a missing binary, an old binary without the verb, or
121
+ * a spawn failure all just report `{ spawned: false, ... }`. A 3 s timer
122
+ * kills a runaway child; the timer is unref'd so it can never keep the host
123
+ * process alive on its own, matching the detached child's own `unref()`.
124
+ */
125
+ export function spawnTelemetryTrigger(options ) {
126
+ const killSwitch = telemetryKillSwitchReason(options.env);
127
+ if (killSwitch !== undefined) return { spawned: false, reason: killSwitch };
128
+ const spawnImpl = options.spawn ?? (nodeSpawn );
129
+ try {
130
+ const child = spawnImpl(options.executable, ["telemetry", "trigger", "--json"], {
131
+ cwd: options.cwd,
132
+ env: options.env,
133
+ detached: true,
134
+ windowsHide: true,
135
+ stdio: "ignore",
136
+ });
137
+ child.on("error", () => {
138
+ // Fire-and-forget: a spawn-time or runtime child error never surfaces
139
+ // to the caller and never affects session start.
140
+ });
141
+ const killTimer = setTimeout(() => {
142
+ try {
143
+ child.kill();
144
+ } catch {
145
+ // Best effort only; the child may already have exited.
146
+ }
147
+ }, TELEMETRY_TRIGGER_KILL_TIMEOUT_MS);
148
+ killTimer.unref?.();
149
+ child.unref();
150
+ return { spawned: true, reason: "spawned" };
151
+ } catch {
152
+ return { spawned: false, reason: "spawn-error" };
153
+ }
154
+ }
@@ -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,6 +38,14 @@ async function main() {
36
38
  if ((mode !== "--write" && mode !== "--check") || process.argv.length !== 3) {
37
39
  throw new Error("usage: build-runtime-modules.mjs --write|--check");
38
40
  }
41
+ // Pi loads these directly as TypeScript. Check syntax without generating
42
+ // another runtime copy or expanding the published module boundary.
43
+ for (const path of ["lib/runtime-metrics-delivery.ts", "lib/runtime-metrics-native.ts",
44
+ "lib/runtime-metrics-children.ts", "extensions/runtime-metrics.ts"]) {
45
+ const source = await readFile(join(root, path), "utf8");
46
+ assertNoTrailingWhitespace(source, path);
47
+ stripTypeScriptTypes(source, { mode: "strip" });
48
+ }
39
49
  const runtime = join(root, "runtime");
40
50
  if (mode === "--write") await mkdir(runtime, { recursive: true });
41
51
  const drift = [];
@@ -57,7 +67,7 @@ async function main() {
57
67
  if (drift.length > 0) {
58
68
  throw new Error(`generated runtime is stale: ${drift.join(", ")}`);
59
69
  }
60
- process.stdout.write(`runtime ${mode === "--write" ? "generated" : "matches TypeScript sources"} (${sources.length} modules)\n`);
70
+ process.stdout.write(`runtime ${mode === "--write" ? "generated" : "matches TypeScript sources"} (${sources.length} generated modules; one-shot metrics sources validated)\n`);
61
71
  }
62
72
 
63
73
  main().catch((error) => {
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ // Type gate for a project that does not compile cleanly yet.
3
+ //
4
+ // `tsc --noEmit` reports a fixed set of diagnostics on this repository today, so
5
+ // the gate is a ratchet rather than a clean pass: it fails when the number of
6
+ // diagnostics grows for any (file, error code) pair, or when the total grows.
7
+ // Fixing diagnostics and refreshing the baseline is the intended way to shrink
8
+ // it.
9
+ //
10
+ // Known blind spot, stated rather than hidden: keying on (file, code) counts is
11
+ // what keeps the baseline stable while files are edited, because line numbers
12
+ // shift. A single change that removes one diagnostic and introduces another of
13
+ // the same code in the same file therefore passes. The total check catches that
14
+ // whenever the counts differ at all.
15
+ //
16
+ // Usage:
17
+ // node scripts/check-types.mjs fail on any new diagnostic
18
+ // node scripts/check-types.mjs --update rewrite the baseline and exit 0
19
+
20
+ import { execFileSync } from "node:child_process";
21
+ import { readFileSync, writeFileSync } from "node:fs";
22
+ import { createRequire } from "node:module";
23
+ import { dirname, join, relative } from "node:path";
24
+ import { fileURLToPath } from "node:url";
25
+
26
+ const packageRoot = dirname(dirname(fileURLToPath(import.meta.url)));
27
+ const baselinePath = join(packageRoot, "scripts", "types-baseline.json");
28
+ const configPath = join(packageRoot, "tsconfig.json");
29
+ const DIAGNOSTIC = /^(.*?)\((\d+),(\d+)\): error (TS\d+): (.*)$/;
30
+ const REPORT_LIMIT = 20;
31
+
32
+ function relativeToPackage(file) {
33
+ return relative(packageRoot, file).split("\\").join("/");
34
+ }
35
+
36
+ function compile() {
37
+ let tsc;
38
+ try {
39
+ tsc = createRequire(import.meta.url).resolve("typescript/bin/tsc");
40
+ } catch {
41
+ console.error("types: cannot resolve the TypeScript compiler. Run `pnpm install` first.");
42
+ process.exit(1);
43
+ }
44
+ try {
45
+ execFileSync(process.execPath, [tsc, "-p", configPath], {
46
+ cwd: packageRoot,
47
+ encoding: "utf8",
48
+ stdio: ["ignore", "pipe", "pipe"],
49
+ });
50
+ return "";
51
+ } catch (error) {
52
+ // tsc exits non-zero when it reports diagnostics and writes them to stdout.
53
+ return `${error.stdout ?? ""}${error.stderr ?? ""}`;
54
+ }
55
+ }
56
+
57
+ function summarize(output) {
58
+ const counts = new Map();
59
+ const samples = new Map();
60
+ let total = 0;
61
+ for (const line of output.split(/\r?\n/)) {
62
+ const match = DIAGNOSTIC.exec(line);
63
+ if (!match) continue;
64
+ const key = `${relativeToPackage(match[1])} ${match[4]}`;
65
+ counts.set(key, (counts.get(key) ?? 0) + 1);
66
+ if (!samples.has(key)) samples.set(key, match[5].trim());
67
+ total += 1;
68
+ }
69
+ return { counts, samples, total };
70
+ }
71
+
72
+ function writeBaseline(summary) {
73
+ const byFileAndCode = Object.fromEntries([...summary.counts].sort(([left], [right]) => left.localeCompare(right)));
74
+ const payload = {
75
+ note: "Recorded TypeScript diagnostics. Regenerate with `node scripts/check-types.mjs --update` after fixing or deliberately accepting diagnostics. This file may only shrink without a review decision.",
76
+ total: summary.total,
77
+ byFileAndCode,
78
+ };
79
+ writeFileSync(baselinePath, `${JSON.stringify(payload, null, 2)}\n`);
80
+ }
81
+
82
+ const summary = summarize(compile());
83
+
84
+ if (process.argv.includes("--update")) {
85
+ writeBaseline(summary);
86
+ console.log(`types: baseline recorded ${summary.total} diagnostic(s) across ${summary.counts.size} file/code pair(s)`);
87
+ process.exit(0);
88
+ }
89
+
90
+ let baseline;
91
+ try {
92
+ baseline = JSON.parse(readFileSync(baselinePath, "utf8"));
93
+ } catch (error) {
94
+ console.error(`types: cannot read ${relativeToPackage(baselinePath)}: ${error instanceof Error ? error.message : String(error)}`);
95
+ process.exit(1);
96
+ }
97
+
98
+ const regressions = [];
99
+ for (const [key, count] of summary.counts) {
100
+ const recorded = baseline.byFileAndCode[key] ?? 0;
101
+ if (count > recorded) regressions.push({ key, added: count - recorded, message: summary.samples.get(key) ?? "" });
102
+ }
103
+
104
+ const totalGrew = summary.total > baseline.total;
105
+
106
+ if (regressions.length > 0 || totalGrew) {
107
+ if (regressions.length > 0) {
108
+ console.error(`types: ${regressions.length} file/code pair(s) report more diagnostics than recorded:`);
109
+ for (const regression of regressions.slice(0, REPORT_LIMIT)) {
110
+ console.error(` +${regression.added} ${regression.key} ${regression.message.slice(0, 100)}`);
111
+ }
112
+ if (regressions.length > REPORT_LIMIT) console.error(` ... and ${regressions.length - REPORT_LIMIT} more`);
113
+ }
114
+ if (totalGrew) {
115
+ console.error(`types: total ${summary.total} exceeds the recorded ${baseline.total}`);
116
+ }
117
+ console.error("Fix the new diagnostics, or run `node scripts/check-types.mjs --update` when they are deliberately accepted.");
118
+ process.exit(1);
119
+ }
120
+
121
+ const improved = [...Object.entries(baseline.byFileAndCode)].filter(([key, count]) => (summary.counts.get(key) ?? 0) < count).length;
122
+ console.log(
123
+ `types: ${summary.total} recorded diagnostic(s), no regressions` +
124
+ (improved > 0 ? `; ${improved} file/code pair(s) improved, run --update to shrink the baseline` : ""),
125
+ );