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
@@ -1,17 +1,20 @@
1
1
  import { CustomEditor, keyHint, type ExtensionAPI, type ExtensionContext, type KeybindingsManager } from "@earendil-works/pi-coding-agent";
2
2
  import type { EditorTheme, TUI } from "@earendil-works/pi-tui";
3
- import { spawnSync } from "node:child_process";
3
+ import { execFile, spawnSync } from "node:child_process";
4
4
  import { readFile } from "node:fs/promises";
5
5
  import * as os from "node:os";
6
6
  import { join } from "node:path";
7
- import { renderShellBar, shellEnabled, type ShellBarModel, type ShellBarTheme } from "../lib/shell-bar.ts";
8
- import { CHANGE_STATUS, ChangesTracker, renderChangesWidget, type ChangedFile, type ChangesModel, type GitRunner, type LineCounter } from "../lib/shell-changes.ts";
9
- import { ChangesView } from "../lib/shell-changes-view.ts";
7
+ import { renderShellBar, renderShellSidebarBar, shellEnabled, type ShellBarModel, type ShellBarTheme } from "../lib/shell-bar.ts";
8
+ import { CHANGE_STATUS, WorktreeChangesTracker, renderChangesWidget, type ChangedFile, type ChangesModel, type GitRunner, type LineCounter, type WorktreeChanges } from "../lib/shell-changes.ts";
9
+ import { WorktreeChangesView } from "../lib/shell-changes-view.ts";
10
+ import { SessionWorktreeRegistry, SESSION_WORKTREE_CHANGED, resolveSessionWorktree, toolWorktreePath, worktreeGitEnvironment, type WorktreeResolver } from "../lib/session-worktree-registry.ts";
10
11
  import { CARD_TONE, renderCard, type Card, type CardTheme } from "../lib/shell-card.ts";
11
12
  import { GentleAiDevBinaryOverrideError, resolveGentleAiDevBinaryOverride } from "../lib/gentle-ai-binary.ts";
12
- import { framePromptLines, panelPainter, PROMPT_HINT, PROMPT_STATE, withPromptHint, type PromptState } from "../lib/shell-prompt.ts";
13
+ import { framePromptLines, PROMPT_HINT, PROMPT_STATE, withPromptHint, type PromptState } from "../lib/shell-prompt.ts";
13
14
  import { accountIdFromToken, CODEX_PROVIDER, CODEX_USAGE_URL, parseCodexUsage, parseUsageHeaders, UsageStore, type ProviderUsage } from "../lib/shell-usage.ts";
14
15
  import { UsageView } from "../lib/shell-usage-view.ts";
16
+ import { sidebarPart } from "../lib/shell-sidebar.ts";
17
+ import { installSidebar, invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
15
18
 
16
19
  // Gentle Shell: the visual layer gentle-pi puts on top of pi. It installs the
17
20
  // status bar, the petal prompt, the working-tree changes widget and overlay,
@@ -26,6 +29,7 @@ export interface ShellFooterData {
26
29
 
27
30
  interface ShellRenderHost {
28
31
  requestRender(): void;
32
+ invalidateSidebar?(): void;
29
33
  }
30
34
 
31
35
  interface ShellBarComponent {
@@ -46,6 +50,8 @@ export interface ShellDeps {
46
50
  fetch: typeof fetch;
47
51
  now(): number;
48
52
  devBinary(): DevBinaryNotice | undefined;
53
+ resolveWorktree: WorktreeResolver;
54
+ gitRunner(cwd: string): GitRunner;
49
55
  }
50
56
 
51
57
  function ambientDevBinary(): DevBinaryNotice | undefined {
@@ -58,7 +64,7 @@ function ambientDevBinary(): DevBinaryNotice | undefined {
58
64
  }
59
65
  }
60
66
 
61
- const defaultShellDeps: ShellDeps = { fetch: (...args) => globalThis.fetch(...args), now: () => Date.now(), devBinary: ambientDevBinary };
67
+ const defaultShellDeps: ShellDeps = { fetch: (...args) => globalThis.fetch(...args), now: () => Date.now(), devBinary: ambientDevBinary, resolveWorktree: resolveSessionWorktree, gitRunner: shellGitRunner };
62
68
 
63
69
  interface AssistantUsageEntry {
64
70
  type: string;
@@ -121,7 +127,10 @@ export function createShellBarComponent(
121
127
  dirty: () => number | undefined = () => undefined,
122
128
  usage: () => ProviderUsage | undefined = () => undefined,
123
129
  ): ShellBarComponent {
124
- const unsubscribe = footerData.onBranchChange(() => host.requestRender());
130
+ const unsubscribe = footerData.onBranchChange(() => {
131
+ host.invalidateSidebar?.();
132
+ host.requestRender();
133
+ });
125
134
  return {
126
135
  render(width: number) {
127
136
  return renderShellBar(buildShellBarModel(pi, ctx, footerData, { dirty: dirty(), usage: usage() }), theme, width);
@@ -136,7 +145,6 @@ export function createShellBarComponent(
136
145
  interface PromptEditorDeps {
137
146
  fg: (color: string, text: string) => string;
138
147
  bold: (text: string) => string;
139
- paint?: (line: string) => string;
140
148
  requestRender(): void;
141
149
  pending(): boolean;
142
150
  }
@@ -181,7 +189,6 @@ export class GentlePromptEditor extends CustomEditor {
181
189
  borderColor: (text) => this.deps.fg(PROMPT_FRAME_ROLE, text),
182
190
  fg: this.deps.fg,
183
191
  bold: this.deps.bold,
184
- paint: this.deps.paint,
185
192
  });
186
193
  }
187
194
 
@@ -202,7 +209,6 @@ function installPrompt(ctx: ExtensionContext, onCreated: (prompt: GentlePromptEd
202
209
  const prompt = new GentlePromptEditor(tui, theme, keybindings, {
203
210
  fg: (color, text) => ctx.ui.theme.fg(color as Parameters<typeof ctx.ui.theme.fg>[0], text),
204
211
  bold: (text) => ctx.ui.theme.bold(text),
205
- paint: panelPainter(ctx.ui.theme.getBgAnsi("customMessageBg")),
206
212
  requestRender: () => tui.requestRender(),
207
213
  pending: () => ctx.hasPendingMessages(),
208
214
  });
@@ -220,11 +226,24 @@ const GIT_TIMEOUT_MS = 5000;
220
226
  const OVERLAY_HEIGHT_RATIO = 0.8;
221
227
  const OVERLAY_MIN_ROWS = 8;
222
228
 
223
- function gitRunner(pi: ExtensionAPI, cwd: string): GitRunner {
224
- return async (args: string[]) => {
225
- const result = await pi.exec("git", ["-C", cwd, ...args], { timeout: GIT_TIMEOUT_MS });
226
- return { stdout: result.stdout, code: result.code };
227
- };
229
+ export function shellGitRunner(cwd: string, env: NodeJS.ProcessEnv = process.env, run: typeof execFile = execFile): GitRunner {
230
+ // Pi exec cannot replace the inherited environment. Use argv directly and
231
+ // a complete sanitized environment for discovery, status, and lazy diffs.
232
+ const childEnv = worktreeGitEnvironment(env);
233
+ return (args) => new Promise((resolve) => {
234
+ run("git", ["-C", cwd, ...args], {
235
+ env: childEnv,
236
+ encoding: "utf8",
237
+ shell: false,
238
+ windowsHide: true,
239
+ timeout: GIT_TIMEOUT_MS,
240
+ // Pi exec accumulates output without a maxBuffer cap. In particular,
241
+ // large porcelain inventories must not become partial successful scans.
242
+ maxBuffer: Infinity,
243
+ }, (error, stdout) => {
244
+ resolve({ stdout, code: error ? typeof error.code === "number" ? error.code : 1 : 0 });
245
+ });
246
+ });
228
247
  }
229
248
 
230
249
  function lineCounter(cwd: string): LineCounter {
@@ -247,13 +266,13 @@ export interface ExternalEditorHost {
247
266
  requestRender(force?: boolean): void;
248
267
  }
249
268
 
250
- export function openInExternalEditor(host: ExternalEditorHost, path: string, env: NodeJS.ProcessEnv = process.env, spawn: typeof spawnSync = spawnSync): boolean {
269
+ export function openInExternalEditor(host: ExternalEditorHost, path: string, env: NodeJS.ProcessEnv = process.env, spawn: typeof spawnSync = spawnSync, cwd?: string): boolean {
251
270
  const command = env.VISUAL || env.EDITOR;
252
271
  if (!command) return false;
253
272
  const [editor, ...editorArgs] = command.split(" ");
254
273
  host.stop();
255
274
  try {
256
- spawn(editor, [...editorArgs, path], { stdio: "inherit", shell: process.platform === "win32" });
275
+ spawn(editor, [...editorArgs, path], { cwd, stdio: "inherit", shell: process.platform === "win32" });
257
276
  } finally {
258
277
  host.start();
259
278
  host.requestRender(true);
@@ -289,7 +308,8 @@ function changesFingerprint(model: ChangesModel): string {
289
308
  }
290
309
 
291
310
  interface OverlayDeps {
292
- git: GitRunner;
311
+ git(root: string): GitRunner;
312
+ worktrees(): WorktreeChanges[];
293
313
  refresh(): Promise<ChangesModel>;
294
314
  apply(ctx: ExtensionContext, model: ChangesModel): void;
295
315
  pollMs: number;
@@ -297,24 +317,26 @@ interface OverlayDeps {
297
317
 
298
318
  // While the overlay is open, git is polled so edits made outside pi (nvim,
299
319
  // another agent, a git checkout) show up without reopening it.
300
- async function showChangesOverlay(ctx: ExtensionContext, model: ChangesModel, deps: OverlayDeps): Promise<void> {
320
+ async function showChangesOverlay(ctx: ExtensionContext, deps: OverlayDeps): Promise<void> {
301
321
  let host: ExternalEditorHost | undefined;
302
- let view: ChangesView | undefined;
303
- const poll = setInterval(async () => {
322
+ let view: WorktreeChangesView | undefined;
323
+ const refresh = async () => {
304
324
  const latest = await deps.refresh();
305
- view?.update(latest);
325
+ view?.update(deps.worktrees());
306
326
  deps.apply(ctx, latest);
307
- }, deps.pollMs);
327
+ };
328
+ const poll = setInterval(() => void refresh(), deps.pollMs);
308
329
  poll.unref();
309
330
  try {
310
- const chosen = await ctx.ui.custom<ChangedFile | null>(
331
+ const chosen = await ctx.ui.custom<{ root: string; file: ChangedFile } | null>(
311
332
  (tui, theme, _keybindings, done) => {
312
333
  host = tui;
313
- view = new ChangesView(model, {
334
+ view = new WorktreeChangesView(deps.worktrees(), {
314
335
  theme,
315
- rows: Math.max(OVERLAY_MIN_ROWS, Math.floor(tui.terminal.rows * OVERLAY_HEIGHT_RATIO)),
316
- loadDiff: (file) => loadFileDiff(deps.git, file),
317
- onOpen: (file) => done(file),
336
+ rows: () => Math.max(OVERLAY_MIN_ROWS, Math.floor(tui.terminal.rows * OVERLAY_HEIGHT_RATIO)),
337
+ loadDiff: (root, file) => loadFileDiff(deps.git(root), file),
338
+ onOpen: (root, file) => done({ root, file }),
339
+ onRefresh: () => void refresh(),
318
340
  onClose: () => done(null),
319
341
  requestRender: () => tui.requestRender(),
320
342
  });
@@ -323,9 +345,11 @@ async function showChangesOverlay(ctx: ExtensionContext, model: ChangesModel, de
323
345
  { overlay: true, overlayOptions: { width: "92%", anchor: "center" } },
324
346
  );
325
347
  if (!chosen || !host) return;
326
- if (!openInExternalEditor(host, chosen.path)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
348
+ if (!openInExternalEditor(host, chosen.file.path, process.env, spawnSync, chosen.root)) ctx.ui.notify("No editor configured. Set $VISUAL or $EDITOR.", "warning");
327
349
  } finally {
328
350
  clearInterval(poll);
351
+ view?.dispose();
352
+ view = undefined;
329
353
  }
330
354
  }
331
355
 
@@ -336,11 +360,25 @@ function showChanges(ctx: ExtensionContext, model: ChangesModel): void {
336
360
  }
337
361
  ctx.ui.setWidget(
338
362
  CHANGES_WIDGET_KEY,
339
- (_tui, theme) => ({
363
+ (tui, theme) => sidebarPart(tui, "changes", {
340
364
  render(width: number) {
341
365
  return renderChangesWidget(model, theme, width);
342
366
  },
343
367
  invalidate() {},
368
+ }, {
369
+ render(width: number) {
370
+ const noun = model.files.length === 1 ? "file" : "files";
371
+ return renderCard({
372
+ title: "Changes",
373
+ tone: CARD_TONE.INFO,
374
+ body: [
375
+ `${model.files.length} ${noun} · ${theme.fg("success", `+${model.added}`)} ${theme.fg("error", `−${model.deleted}`)}`,
376
+ "",
377
+ theme.fg("muted", `/${CHANGES_COMMAND_NAME}`),
378
+ ],
379
+ }, theme, width, { expanded: true });
380
+ },
381
+ invalidate() {},
344
382
  }),
345
383
  { placement: "belowEditor" },
346
384
  );
@@ -359,7 +397,6 @@ function messageText(content: string | Array<{ type: string; text?: string }>):
359
397
  interface CardComponentOptions {
360
398
  expanded: boolean;
361
399
  hint?: string;
362
- paint?: (line: string) => string;
363
400
  }
364
401
 
365
402
  function cardComponent(card: Card, theme: CardTheme, options: CardComponentOptions) {
@@ -429,12 +466,14 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
429
466
  const fetched = await fetchCodexUsage(token, deps.fetch, deps.now());
430
467
  if (!fetched) return;
431
468
  usage.record(fetched);
469
+ renderHost?.invalidateSidebar?.();
432
470
  renderHost?.requestRender();
433
471
  };
434
472
  pi.on("after_provider_response", (event) => {
435
473
  const parsed = parseUsageHeaders(event.headers, deps.now());
436
474
  if (!parsed) return;
437
475
  usage.record(parsed);
476
+ renderHost?.invalidateSidebar?.();
438
477
  renderHost?.requestRender();
439
478
  });
440
479
  pi.registerMessageRenderer(REVIEW_PREFLIGHT_TYPE, (message, options, theme) => {
@@ -461,7 +500,10 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
461
500
  },
462
501
  });
463
502
  let prompt: GentlePromptEditor | undefined;
464
- let changes: ChangesTracker | undefined;
503
+ let changes: WorktreeChangesTracker | undefined;
504
+ let registry: SessionWorktreeRegistry | undefined;
505
+ let currentContext: ExtensionContext | undefined;
506
+ const pendingTools = new Map<string, { sessionId: string; path?: string }>();
465
507
  let watch: NodeJS.Timeout | undefined;
466
508
  let shown = "";
467
509
  const applyChanges = (ctx: ExtensionContext, model: ChangesModel) => {
@@ -471,20 +513,59 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
471
513
  showChanges(ctx, model);
472
514
  };
473
515
  const refreshChanges = async (ctx: ExtensionContext) => {
474
- if (!changes || !ctx.hasUI) return;
475
- applyChanges(ctx, await changes.refresh());
516
+ const tracker = changes;
517
+ if (!tracker || !ctx.hasUI || registry?.sessionId !== ctx.sessionManager.getSessionId()) return;
518
+ const model = await tracker.refresh();
519
+ if (changes === tracker) applyChanges(ctx, model);
476
520
  };
477
521
  const stopWatch = () => {
478
522
  if (watch) clearInterval(watch);
479
523
  watch = undefined;
480
524
  };
525
+ const unsubscribeWorktrees = pi.events.on(SESSION_WORKTREE_CHANGED, (data) => {
526
+ if (!currentContext || !registry || (data as { sessionId?: string } | undefined)?.sessionId !== registry.sessionId) return;
527
+ void refreshChanges(currentContext);
528
+ });
529
+ pi.registerTool({
530
+ name: "session_worktree_register",
531
+ renderShell: "self",
532
+ label: "Register session worktree",
533
+ description: "Register a worktree used by this session, including earlier work or opaque shell use. Only the same Git clone is accepted. Shows ALL dirty files in that root, including preexisting and untracked files; never infers roots from shell commands or prose.",
534
+ parameters: { type: "object", required: ["path"], additionalProperties: false, properties: { path: { type: "string", description: "Worktree path to include in this session." } } } as never,
535
+ async execute(_id, params, _signal, _onUpdate, ctx) {
536
+ if (!registry || registry.sessionId !== ctx.sessionManager.getSessionId()) throw new Error("No active session worktree registry.");
537
+ const root = registry.register((params as { path: string }).path, "explicit");
538
+ await refreshChanges(ctx);
539
+ return { content: [{ type: "text", text: `Registered session worktree: ${root}` }], details: { root, sessionId: registry.sessionId } };
540
+ },
541
+ });
481
542
  pi.on("session_start", async (_event, ctx) => {
543
+ stopWatch();
544
+ registry?.close();
545
+ pendingTools.clear();
546
+ currentContext = ctx;
547
+ changes = undefined;
548
+ registry = new SessionWorktreeRegistry(pi, ctx.sessionManager, ctx.cwd, deps.resolveWorktree);
549
+ registry.start();
550
+ const sessionRegistry = registry;
482
551
  if (!ctx.hasUI) return;
483
- changes = new ChangesTracker(gitRunner(pi, ctx.cwd), lineCounter(ctx.cwd));
552
+ changes = new WorktreeChangesTracker(deps.gitRunner(ctx.cwd), deps.gitRunner, lineCounter, () => sessionRegistry.roots());
484
553
  const tracker = changes;
485
554
  ctx.ui.setFooter((tui, theme, footerData) => {
486
- renderHost = tui;
487
- return createShellBarComponent(pi, ctx, tui, theme, footerData, () => tracker.model.files.length, () => usage.get(ctx.model?.provider ?? ""));
555
+ renderHost = { requestRender: () => tui.requestRender(), invalidateSidebar: () => invalidateSidebar(tui) };
556
+ const bottom = createShellBarComponent(pi, ctx, renderHost, theme, footerData, () => tracker.model.files.length, () => usage.get(ctx.model?.provider ?? ""));
557
+ // The Status card paints live session state that no event re-registers a
558
+ // part for: model, effort, context, cost, session name and extension
559
+ // statuses. The digest is what keeps the fullscreen memo honest, and it
560
+ // rebuilds the model exactly as the narrow bottom bar does every frame.
561
+ const footerModel = () => buildShellBarModel(pi, ctx, footerData, { dirty: tracker.model.files.length, usage: usage.get(ctx.model?.provider ?? "") });
562
+ const part = sidebarPart(tui, "footer", bottom, {
563
+ digest: () => JSON.stringify(footerModel()),
564
+ render: (width) => renderShellSidebarBar(footerModel(), theme, width),
565
+ invalidate() {},
566
+ });
567
+ const uninstall = installSidebar(tui, theme);
568
+ return { ...part, dispose() { uninstall(); part.dispose(); } };
488
569
  });
489
570
  void refreshUsage(ctx, true);
490
571
  installPrompt(ctx, (created) => {
@@ -496,10 +577,11 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
496
577
  ctx.ui.setWidget(
497
578
  DEV_BINARY_WIDGET_KEY,
498
579
  notice
499
- ? (_tui, theme) => spaced(cardComponent(devBinaryCard(notice), theme, { expanded: true, paint: (line) => theme.bg("customMessageBg", line) }))
580
+ ? (_tui, theme) => spaced(cardComponent(devBinaryCard(notice), theme, { expanded: true }))
500
581
  : undefined,
501
582
  );
502
583
  await tracker.start();
584
+ if (changes !== tracker) return;
503
585
  shown = "";
504
586
  applyChanges(ctx, tracker.model);
505
587
  stopWatch();
@@ -509,7 +591,15 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
509
591
  watch.unref();
510
592
  }
511
593
  });
512
- pi.on("session_shutdown", () => stopWatch());
594
+ pi.on("session_shutdown", () => {
595
+ stopWatch();
596
+ registry?.close();
597
+ registry = undefined;
598
+ changes = undefined;
599
+ currentContext = undefined;
600
+ pendingTools.clear();
601
+ unsubscribeWorktrees();
602
+ });
513
603
  const openChanges = async (ctx: ExtensionContext) => {
514
604
  if (!changes) return;
515
605
  const tracker = changes;
@@ -518,10 +608,10 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
518
608
  ctx.ui.notify("No changes in the working tree.", "info");
519
609
  return;
520
610
  }
521
- await showChangesOverlay(ctx, model, { git: gitRunner(pi, ctx.cwd), refresh: () => tracker.refresh(), apply: applyChanges, pollMs: changesPollMs(env) });
611
+ await showChangesOverlay(ctx, { git: deps.gitRunner, worktrees: () => tracker.worktrees, refresh: () => tracker.refresh(), apply: applyChanges, pollMs: changesPollMs(env) });
522
612
  };
523
613
  pi.registerCommand(CHANGES_COMMAND_NAME, {
524
- description: "Show the working tree changes against HEAD, with diffs. Press o to open a file in $EDITOR.",
614
+ description: "Browse this session's registered dirty worktrees and their changes against HEAD. Press o on a file to open $EDITOR.",
525
615
  handler: async (_args, ctx) => openChanges(ctx),
526
616
  });
527
617
  const shortcut = changesShortcut(env);
@@ -541,7 +631,20 @@ export default function gentleShell(pi: ExtensionAPI, env: NodeJS.ProcessEnv = p
541
631
  await refreshChanges(ctx);
542
632
  void refreshUsage(ctx, false);
543
633
  });
544
- pi.on("tool_execution_end", async (_event, ctx) => {
634
+ pi.on("tool_execution_start", (event, ctx) => {
635
+ pendingTools.set(event.toolCallId, { sessionId: ctx.sessionManager.getSessionId() });
636
+ });
637
+ pi.on("tool_result", (event, ctx) => {
638
+ const pending = pendingTools.get(event.toolCallId);
639
+ if (pending?.sessionId === ctx.sessionManager.getSessionId()) pending.path = toolWorktreePath(event.toolName, event.input);
640
+ });
641
+ pi.on("tool_execution_end", async (event, ctx) => {
642
+ const pending = pendingTools.get(event.toolCallId);
643
+ pendingTools.delete(event.toolCallId);
644
+ if (!event.isError && pending?.path !== undefined && pending.sessionId === registry?.sessionId && pending.sessionId === ctx.sessionManager.getSessionId()) {
645
+ try { registry.register(pending.path, `tool:${event.toolName}`); }
646
+ catch { /* Non-project paths and missing roots do not expand the registry. */ }
647
+ }
545
648
  await refreshChanges(ctx);
546
649
  });
547
650
  }
@@ -1,5 +1,8 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { Text } from "@earendil-works/pi-tui";
2
+ import { Text, type Component, type TUI } from "@earendil-works/pi-tui";
3
+ import { NativePointerRegion } from "../lib/native-pointer-region.ts";
4
+ import { sidebarPart } from "../lib/shell-sidebar.ts";
5
+ import { invalidateSidebar } from "../lib/shell-sidebar-layout.ts";
3
6
  import {
4
7
  applyTodo,
5
8
  emptyTodo,
@@ -72,6 +75,7 @@ interface TodoSession {
72
75
  clearOnNextTurn: boolean;
73
76
  ui: ExtensionContext["ui"] | undefined;
74
77
  host: { requestRender(): void } | undefined;
78
+ tui: TUI | undefined;
75
79
  }
76
80
 
77
81
  function sessionKey(ctx: ExtensionContext): string {
@@ -87,13 +91,48 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
87
91
  const key = sessionKey(ctx);
88
92
  let current = sessions.get(key);
89
93
  if (!current) {
90
- current = { state: emptyTodo(), turn: 0, collapsed: false, clearOnNextTurn: false, ui: undefined, host: undefined };
94
+ current = { state: emptyTodo(), turn: 0, collapsed: false, clearOnNextTurn: false, ui: undefined, host: undefined, tui: undefined };
91
95
  sessions.set(key, current);
92
96
  }
93
97
  return current;
94
98
  };
95
99
 
100
+ const toggle = (current: TodoSession) => {
101
+ current.collapsed = !current.collapsed;
102
+ if (current.tui) invalidateSidebar(current.tui);
103
+ current.host?.requestRender();
104
+ };
105
+
106
+ const todoCard = (current: TodoSession, theme: Parameters<typeof renderTodoCard>[1], scrollable: boolean, spacer: boolean): Component & { dispose(): void } => {
107
+ const card: Component = {
108
+ render(width: number) {
109
+ const lines = renderTodoCard(current.state, theme, width, {
110
+ collapsed: current.collapsed,
111
+ staleTurns: staleTurns(current.state, current.turn),
112
+ collapseKey,
113
+ ...(scrollable ? { scrollable: true } : {}),
114
+ });
115
+ return spacer && lines.length > 0 ? [...lines, ""] : lines;
116
+ },
117
+ invalidate() {},
118
+ };
119
+ const region = new NativePointerRegion(card, {
120
+ onClick(event) {
121
+ if (event.button !== "left" || event.y !== 0) return undefined;
122
+ toggle(current);
123
+ return { handled: true, render: true };
124
+ },
125
+ });
126
+ return {
127
+ render: (width) => region.render(width),
128
+ handleMouse: (event) => region.handleMouse(event),
129
+ invalidate: () => region.invalidate(),
130
+ dispose: () => region.dispose(),
131
+ };
132
+ };
133
+
96
134
  const show = (current: TodoSession) => {
135
+ if (current.tui) invalidateSidebar(current.tui);
97
136
  if (!current.ui) return;
98
137
  if (current.state.tasks.length === 0) {
99
138
  current.ui.setWidget(WIDGET_KEY, undefined);
@@ -102,18 +141,14 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
102
141
  const snapshot = current;
103
142
  current.ui.setWidget(WIDGET_KEY, (tui, theme) => {
104
143
  snapshot.host = tui;
105
- return {
106
- render(width: number) {
107
- const lines = renderTodoCard(snapshot.state, theme, width, { collapsed: snapshot.collapsed, staleTurns: staleTurns(snapshot.state, snapshot.turn), collapseKey });
108
- return lines.length === 0 ? [] : [...lines, ""];
109
- },
110
- invalidate() {},
111
- };
144
+ snapshot.tui = tui;
145
+ return sidebarPart(tui, "todo", todoCard(snapshot, theme, false, true), todoCard(snapshot, theme, true, false));
112
146
  });
113
147
  };
114
148
 
115
149
  pi.registerTool({
116
150
  name: TODO_TOOL_NAME,
151
+ renderShell: "self",
117
152
  label: "Todo",
118
153
  description: "Plan and track multi-step work. Use write to set the whole list, update to move one task, add for a new one, clear to reset, list to read it back.",
119
154
  promptSnippet: "Track multi-step work; rewrite the whole list as the plan changes",
@@ -139,6 +174,7 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
139
174
  if (!result.error) {
140
175
  current.state = result.state;
141
176
  current.clearOnNextTurn = false;
177
+ if (current.tui) invalidateSidebar(current.tui);
142
178
  }
143
179
  return {
144
180
  content: [{ type: "text", text: result.text }],
@@ -151,9 +187,7 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
151
187
  pi.registerShortcut(collapseKey as Parameters<ExtensionAPI["registerShortcut"]>[0], {
152
188
  description: "Collapse or expand the todo list",
153
189
  handler: async (ctx) => {
154
- const current = session(ctx);
155
- current.collapsed = !current.collapsed;
156
- current.host?.requestRender();
190
+ toggle(session(ctx));
157
191
  },
158
192
  });
159
193
  }
@@ -176,6 +210,7 @@ export default function gentleTodo(pi: ExtensionAPI, env: NodeJS.ProcessEnv = pr
176
210
  pi.on("before_agent_start", (event, ctx) => {
177
211
  const current = session(ctx);
178
212
  current.turn += 1;
213
+ if (current.tui) invalidateSidebar(current.tui);
179
214
  if (current.clearOnNextTurn) {
180
215
  current.state = { ...current.state, tasks: [] };
181
216
  current.clearOnNextTurn = false;
@@ -625,6 +625,7 @@ function registerQuietTool(pi: ExtensionAPI, toolName: QuietToolName, commandArg
625
625
 
626
626
  pi.registerTool({
627
627
  ...registrationTool,
628
+ renderShell: "self",
628
629
  async execute(toolCallId, params, signal, onUpdate, ctx) {
629
630
  const runtimeTool = getBuiltInTools(ctx.cwd)[toolName];
630
631
  return runtimeTool.execute(toolCallId, params, signal, onUpdate, ctx);
@@ -0,0 +1,130 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { EFFORTS, ORCHESTRATOR_AGENT_CLASS, RuntimeMetrics, UNKNOWN_AGENT_CLASS, type FinalResponse, type TokenMeasurement } from "../lib/runtime-metrics.ts";
3
+ import { classifyPiCatalogName, lookupPiCatalogName } from "../lib/runtime-metrics-pi-identity.ts";
4
+ import { CHILD_METRICS_EVENT, CHILD_METRICS_REVOKED, snapshotChildEvent, type ChildLaunchBucket } from "../lib/runtime-metrics-children.ts";
5
+ import { RuntimeMetricsAttempt } from "../lib/runtime-metrics-delivery.ts";
6
+ import { sendNativeRuntimeEvent, type NativeRuntimeTransportDeps } from "../lib/runtime-metrics-native.ts";
7
+ import { runtimeMetricsEnvAllows } from "../lib/runtime-metrics-policy.ts";
8
+
9
+ /** Available final usage -> one deferred attempt -> discard. No history reads,
10
+ * cumulative session accounting, policy leases, delivery queue or retry. Print
11
+ * mode alone joins its accepted attempt for at most 1.5 seconds at shutdown.
12
+ * Pi hooks lack request correlation: latency and SDK-zero presence are unknown.
13
+ */
14
+ export default function runtimeMetrics(pi: ExtensionAPI, env = process.env,
15
+ { lookup = lookupPiCatalogName, classify = classifyPiCatalogName, native, send = sendNativeRuntimeEvent,
16
+ now = () => performance.now(), shutdownWaitMs = 1500 }:
17
+ { lookup?: typeof lookupPiCatalogName; classify?: typeof classifyPiCatalogName; native?: NativeRuntimeTransportDeps;
18
+ send?: typeof sendNativeRuntimeEvent; now?: () => number; shutdownWaitMs?: number } = {}): void {
19
+ const allows = () => env.GENTLE_PI_AGENTS_CHILD !== "1" && runtimeMetricsEnvAllows(env);
20
+ if (!allows()) return;
21
+ type Selection = Pick<FinalResponse, "selectedModelId" | "selectedProvider" | "effort">;
22
+ let selection: Selection | undefined;
23
+ let active = false;
24
+ let requestSeen = false;
25
+ let ambiguous = false;
26
+ let live: { id: string; started: number; ctx: ExtensionContext; attempt: RuntimeMetricsAttempt;
27
+ seen: WeakSet<object>; children: Set<string>; catalog: boolean } | undefined;
28
+ const missing = { state: "unavailable" } as const;
29
+ function invalidate() { selection = undefined; ambiguous = true; }
30
+ function dispose() { live?.attempt.dispose(); live = undefined; active = false; invalidate(); }
31
+ function current(ctx: ExtensionContext) {
32
+ if (!allows() || live?.id !== ctx.sessionManager.getSessionId()) dispose();
33
+ return live;
34
+ }
35
+ function refreshCatalog(owner: NonNullable<typeof live>, input: { provider: unknown; modelId: unknown }) {
36
+ void lookup(input).then(result => {
37
+ if (live === owner && result.classification === "catalog_public") owner.catalog = true;
38
+ }, () => { /* A later classification callback may retry the bounded load. */ });
39
+ }
40
+ function submit(owner: NonNullable<typeof live>, responses: FinalResponse[], launches?: ChildLaunchBucket[]) {
41
+ if (!responses.length || live !== owner || !current(owner.ctx)) return;
42
+ // Ephemeral event-local accounting only; source IDs never enter these rows.
43
+ const metrics = new RuntimeMetrics({ classifyModel: classify });
44
+ for (const [index, row] of responses.entries()) metrics.record({ ...row, responseId: String(index) });
45
+ const rows = metrics.snapshot();
46
+ if (!rows.length) return;
47
+ const cwd = owner.ctx.cwd;
48
+ owner.attempt.offer(async signal => {
49
+ if (live !== owner || !current(owner.ctx) || signal.aborted) return;
50
+ await send(rows, cwd, { ...native, launches, env, signal,
51
+ current: () => live === owner && current(owner.ctx) === owner });
52
+ });
53
+ }
54
+ const offChild = pi.events.on(CHILD_METRICS_EVENT, value => {
55
+ try {
56
+ const owner = live && current(live.ctx);
57
+ const event = snapshotChildEvent(value);
58
+ if (!owner || !event || event.parentSessionId !== owner.id || event.launchedAt < owner.started
59
+ || owner.children.has(event.taskId) || owner.children.size >= 256) return;
60
+ owner.children.add(event.taskId); // Busy/failed completions stay consumed.
61
+ submit(owner, event.responses, [{ ...event.launch, evidence: "launch_configuration", launches: 1 }]);
62
+ } catch { /* No telemetry error enters the shared event bus. */ }
63
+ });
64
+ const offRevoke = pi.events.on(CHILD_METRICS_REVOKED, id => {
65
+ if (live?.id === id) {
66
+ live.attempt.dispose();
67
+ live.attempt = new RuntimeMetricsAttempt();
68
+ invalidate();
69
+ }
70
+ });
71
+ pi.on("session_start", (_event, ctx) => {
72
+ dispose();
73
+ if (!allows()) return;
74
+ const owner = { id: ctx.sessionManager.getSessionId(), started: now(), ctx,
75
+ attempt: new RuntimeMetricsAttempt(), seen: new WeakSet<object>(), children: new Set<string>(), catalog: false };
76
+ live = owner;
77
+ refreshCatalog(owner, { provider: "openai", modelId: "gpt-4o" });
78
+ });
79
+ pi.on("session_shutdown", (_event, ctx) => {
80
+ const teardown = () => { dispose(); offChild(); offRevoke(); };
81
+ const owner = live;
82
+ if (ctx.mode !== "print" || !owner) { teardown(); return; }
83
+ return owner.attempt.waitForSettled(shutdownWaitMs).finally(teardown);
84
+ });
85
+ pi.on("turn_start", () => { ambiguous = active; active = true; requestSeen = false; selection = undefined; });
86
+ pi.on("turn_end", () => { active = false; invalidate(); });
87
+ pi.on("agent_end", () => { active = false; invalidate(); });
88
+ pi.on("session_before_compact", invalidate);
89
+ pi.on("session_before_tree", invalidate);
90
+ pi.on("before_provider_request", (_event, ctx) => {
91
+ const owner = current(ctx);
92
+ if (!owner) return;
93
+ if (requestSeen) invalidate();
94
+ requestSeen = true;
95
+ if (!active || ambiguous) return;
96
+ let effort: FinalResponse["effort"] = "unavailable";
97
+ try { const value = pi.getThinkingLevel(); if (EFFORTS.includes(value)) effort = value; } catch { /* No evidence. */ }
98
+ selection = { selectedProvider: owner.catalog ? ctx.model?.provider as FinalResponse["provider"] ?? "unknown" : "unknown",
99
+ selectedModelId: owner.catalog && typeof ctx.model?.id === "string" && ctx.model.id.length <= 128 ? ctx.model.id : undefined, effort };
100
+ });
101
+ function token(value: unknown): TokenMeasurement {
102
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 && value <= 1_000_000_000
103
+ ? { state: "reported", value } : missing;
104
+ }
105
+ pi.on("message_end", (event, ctx) => {
106
+ try {
107
+ const owner = current(ctx);
108
+ const message = event.message;
109
+ if (!owner || message.role !== "assistant" || message.stopReason === "pending" || message.stopReason === "deferred"
110
+ || owner.seen.has(message)) return;
111
+ owner.seen.add(message);
112
+ refreshCatalog(owner, { provider: "openai", modelId: "gpt-4o" });
113
+ const selected = active && !ambiguous ? selection : undefined;
114
+ submit(owner, [{ kind: "final_assistant_response", responseId: "0",
115
+ selectedProvider: selected?.selectedProvider ?? "unknown", selectedModelId: selected?.selectedModelId,
116
+ effort: selected?.effort ?? "unavailable",
117
+ executor: env.GENTLE_PI_AGENTS_CHILD === undefined ? "orchestrator" : "unknown",
118
+ agentClass: env.GENTLE_PI_AGENTS_CHILD === undefined ? ORCHESTRATOR_AGENT_CLASS : UNKNOWN_AGENT_CLASS,
119
+ observedModelId: message.model, responseModelId: message.responseModel,
120
+ providerThinkingLevel: EFFORTS.includes(message.providerThinkingLevel as FinalResponse["effort"])
121
+ ? message.providerThinkingLevel as FinalResponse["effort"] : "unavailable",
122
+ provider: message.provider as FinalResponse["provider"], modelFamily: "unknown",
123
+ error: message.stopReason === "aborted" ? "aborted" : message.stopReason === "error" ? "unknown" : "none",
124
+ tokens: { input: token(message.usage?.input), output: token(message.usage?.output), cacheRead: token(message.usage?.cacheRead),
125
+ cacheWrite: token(message.usage?.cacheWrite), reasoning: token(message.usage?.reasoning), totalTokens: token(message.usage?.totalTokens) },
126
+ responseHeadersMs: missing, fullResponseMs: missing }]);
127
+ invalidate();
128
+ } catch { /* Provider hooks never expose telemetry failures. */ }
129
+ });
130
+ }
@@ -7,7 +7,7 @@ import {
7
7
  } from "node:fs";
8
8
  import { basename, dirname, join, relative } from "node:path";
9
9
  import { applySavedModelConfig } from "./gentle-ai.ts";
10
- import { ensureSddPreflight, installSddAssets } from "../lib/sdd-preflight.ts";
10
+ import { ensureSddPreflight, installPackageAssets } from "../lib/sdd-preflight.ts";
11
11
  type ExtensionAPI = any;
12
12
 
13
13
  const CONFIG_REL_PATH = "openspec/config.yaml";
@@ -775,7 +775,7 @@ export default function (pi: ExtensionAPI) {
775
775
  description:
776
776
  "Auto-detect project stack and bootstrap openspec/config.yaml for SDD.",
777
777
  handler: async (_args: unknown, ctx: any) => {
778
- const prefs = await ensureSddPreflight(ctx, { pi, installAssets: (cwd) => installSddAssets(cwd, false), applyModelConfig: () => applySavedModelConfig(ctx) }, { promptFields: [] });
778
+ const prefs = await ensureSddPreflight(ctx, { pi, installAssets: (cwd) => installPackageAssets(cwd, true, ["sdd"]), applyModelConfig: () => applySavedModelConfig(ctx) }, { promptFields: [] });
779
779
 
780
780
  const detection = detectProject(ctx.cwd);
781
781
  const testSummary = detection.testCommand