gsd-pi 2.38.0-dev.96dc7fb → 2.38.0-dev.98b44dc

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 (217) hide show
  1. package/README.md +15 -11
  2. package/dist/app-paths.js +1 -1
  3. package/dist/extension-registry.js +2 -2
  4. package/dist/remote-questions-config.js +2 -2
  5. package/dist/resource-loader.js +34 -1
  6. package/dist/resources/extensions/browser-tools/index.js +3 -1
  7. package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
  8. package/dist/resources/extensions/env-utils.js +29 -0
  9. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  10. package/dist/resources/extensions/github-sync/cli.js +284 -0
  11. package/dist/resources/extensions/github-sync/index.js +73 -0
  12. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  13. package/dist/resources/extensions/github-sync/sync.js +424 -0
  14. package/dist/resources/extensions/github-sync/templates.js +118 -0
  15. package/dist/resources/extensions/github-sync/types.js +7 -0
  16. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  17. package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
  18. package/dist/resources/extensions/gsd/auto-loop.js +636 -594
  19. package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
  20. package/dist/resources/extensions/gsd/auto-prompts.js +202 -48
  21. package/dist/resources/extensions/gsd/auto-start.js +7 -1
  22. package/dist/resources/extensions/gsd/auto-worktree-sync.js +2 -1
  23. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  24. package/dist/resources/extensions/gsd/auto.js +143 -96
  25. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  26. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  27. package/dist/resources/extensions/gsd/commands.js +4 -2
  28. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  29. package/dist/resources/extensions/gsd/detection.js +1 -2
  30. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  31. package/dist/resources/extensions/gsd/doctor-providers.js +30 -11
  32. package/dist/resources/extensions/gsd/doctor.js +20 -1
  33. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  34. package/dist/resources/extensions/gsd/export.js +1 -1
  35. package/dist/resources/extensions/gsd/files.js +48 -9
  36. package/dist/resources/extensions/gsd/forensics.js +1 -1
  37. package/dist/resources/extensions/gsd/git-service.js +30 -12
  38. package/dist/resources/extensions/gsd/gitignore.js +16 -3
  39. package/dist/resources/extensions/gsd/guided-flow.js +149 -38
  40. package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
  41. package/dist/resources/extensions/gsd/health-widget.js +3 -86
  42. package/dist/resources/extensions/gsd/index.js +24 -20
  43. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  44. package/dist/resources/extensions/gsd/migrate-external.js +18 -1
  45. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  46. package/dist/resources/extensions/gsd/paths.js +3 -0
  47. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  48. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  49. package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
  50. package/dist/resources/extensions/gsd/preferences.js +22 -11
  51. package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
  52. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  53. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  54. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  55. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -3
  56. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  57. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  58. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  59. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  60. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  61. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  62. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  63. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  64. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  65. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  66. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  67. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  68. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  69. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  70. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  71. package/dist/resources/extensions/gsd/prompts/run-uat.md +28 -11
  72. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  73. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  74. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  75. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  76. package/dist/resources/extensions/gsd/state.js +42 -23
  77. package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
  78. package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
  79. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  80. package/dist/resources/extensions/mcp-client/index.js +14 -1
  81. package/dist/resources/extensions/remote-questions/status.js +4 -1
  82. package/dist/resources/extensions/remote-questions/store.js +4 -1
  83. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  84. package/dist/resources/extensions/shared/frontmatter.js +1 -1
  85. package/dist/resources/extensions/subagent/isolation.js +2 -1
  86. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  87. package/package.json +1 -1
  88. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  89. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  90. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  91. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  93. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
  95. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/skills.js +6 -1
  97. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  99. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/index.js +1 -1
  101. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  102. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  103. package/packages/pi-coding-agent/src/core/skills.ts +9 -1
  104. package/packages/pi-coding-agent/src/index.ts +1 -0
  105. package/src/resources/extensions/browser-tools/index.ts +3 -0
  106. package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
  107. package/src/resources/extensions/env-utils.ts +31 -0
  108. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  109. package/src/resources/extensions/github-sync/cli.ts +364 -0
  110. package/src/resources/extensions/github-sync/index.ts +93 -0
  111. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  112. package/src/resources/extensions/github-sync/sync.ts +556 -0
  113. package/src/resources/extensions/github-sync/templates.ts +183 -0
  114. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  115. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  116. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  117. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  118. package/src/resources/extensions/github-sync/types.ts +47 -0
  119. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  120. package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
  121. package/src/resources/extensions/gsd/auto-loop.ts +526 -545
  122. package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
  123. package/src/resources/extensions/gsd/auto-prompts.ts +247 -50
  124. package/src/resources/extensions/gsd/auto-start.ts +11 -1
  125. package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -1
  126. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  127. package/src/resources/extensions/gsd/auto.ts +139 -101
  128. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  129. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  130. package/src/resources/extensions/gsd/commands.ts +5 -3
  131. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  132. package/src/resources/extensions/gsd/detection.ts +2 -2
  133. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  134. package/src/resources/extensions/gsd/doctor-providers.ts +30 -9
  135. package/src/resources/extensions/gsd/doctor.ts +22 -1
  136. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  137. package/src/resources/extensions/gsd/export.ts +1 -1
  138. package/src/resources/extensions/gsd/files.ts +51 -11
  139. package/src/resources/extensions/gsd/forensics.ts +1 -1
  140. package/src/resources/extensions/gsd/git-service.ts +44 -10
  141. package/src/resources/extensions/gsd/gitignore.ts +17 -3
  142. package/src/resources/extensions/gsd/guided-flow.ts +177 -44
  143. package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
  144. package/src/resources/extensions/gsd/health-widget.ts +3 -89
  145. package/src/resources/extensions/gsd/index.ts +24 -17
  146. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  147. package/src/resources/extensions/gsd/migrate-external.ts +18 -1
  148. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  149. package/src/resources/extensions/gsd/paths.ts +4 -0
  150. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  151. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  152. package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
  153. package/src/resources/extensions/gsd/preferences.ts +25 -11
  154. package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
  155. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  156. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  157. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  158. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -3
  159. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  160. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  161. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  162. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  163. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  164. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  165. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  166. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  167. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  168. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  169. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  170. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  171. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  172. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  173. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  174. package/src/resources/extensions/gsd/prompts/run-uat.md +28 -11
  175. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  176. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  177. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  178. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  179. package/src/resources/extensions/gsd/state.ts +39 -21
  180. package/src/resources/extensions/gsd/templates/runtime.md +21 -0
  181. package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
  182. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  183. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  184. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
  185. package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
  186. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  187. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
  188. package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
  189. package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
  190. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
  191. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  192. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  193. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  194. package/src/resources/extensions/gsd/tests/run-uat.test.ts +16 -4
  195. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
  196. package/src/resources/extensions/gsd/types.ts +18 -1
  197. package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
  198. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  199. package/src/resources/extensions/mcp-client/index.ts +17 -1
  200. package/src/resources/extensions/remote-questions/status.ts +5 -1
  201. package/src/resources/extensions/remote-questions/store.ts +5 -1
  202. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  203. package/src/resources/extensions/shared/frontmatter.ts +1 -1
  204. package/src/resources/extensions/subagent/isolation.ts +3 -1
  205. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  206. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  207. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  208. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  209. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  210. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  211. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  212. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  213. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  214. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  215. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  216. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  217. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -297,116 +297,163 @@ export async function stopAuto(ctx, pi, reason) {
297
297
  return;
298
298
  const loadedPreferences = loadEffectiveGSDPreferences()?.preferences;
299
299
  const reasonSuffix = reason ? ` — ${reason}` : "";
300
- clearUnitTimeout();
301
- if (lockBase())
302
- clearLock(lockBase());
303
- if (lockBase())
304
- releaseSessionLock(lockBase());
305
- clearSkillSnapshot();
306
- resetSkillTelemetry();
307
- // Remove SIGTERM handler registered at auto-mode start
308
- deregisterSigtermHandler();
309
- // ── Auto-worktree: exit worktree and reset s.basePath on stop ──
310
- if (s.currentMilestoneId) {
311
- const notifyCtx = ctx
312
- ? { notify: ctx.ui.notify.bind(ctx.ui) }
313
- : { notify: () => { } };
314
- buildResolver().exitMilestone(s.currentMilestoneId, notifyCtx, {
315
- preserveBranch: true,
316
- });
317
- }
318
- // ── DB cleanup: close the SQLite connection ──
319
- if (isDbAvailable()) {
300
+ try {
301
+ // ── Step 1: Timers and locks ──
320
302
  try {
321
- const { closeDatabase } = await import("./gsd-db.js");
322
- closeDatabase();
303
+ clearUnitTimeout();
304
+ if (lockBase())
305
+ clearLock(lockBase());
306
+ if (lockBase())
307
+ releaseSessionLock(lockBase());
323
308
  }
324
309
  catch (e) {
325
- debugLog("db-close-failed", {
326
- error: e instanceof Error ? e.message : String(e),
327
- });
310
+ debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
328
311
  }
329
- }
330
- if (s.originalBasePath) {
331
- s.basePath = s.originalBasePath;
312
+ // ── Step 2: Skill state ──
332
313
  try {
333
- process.chdir(s.basePath);
314
+ clearSkillSnapshot();
315
+ resetSkillTelemetry();
334
316
  }
335
- catch {
336
- /* best-effort */
317
+ catch (e) {
318
+ debugLog("stop-cleanup-skills", { error: e instanceof Error ? e.message : String(e) });
337
319
  }
338
- }
339
- const ledger = getLedger();
340
- if (ledger && ledger.units.length > 0) {
341
- const totals = getProjectTotals(ledger.units);
342
- ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`, "info");
343
- }
344
- else {
345
- ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
346
- }
347
- if (s.basePath) {
320
+ // ── Step 3: SIGTERM handler ──
348
321
  try {
349
- await rebuildState(s.basePath);
322
+ deregisterSigtermHandler();
350
323
  }
351
324
  catch (e) {
352
- debugLog("stop-rebuild-state-failed", {
353
- error: e instanceof Error ? e.message : String(e),
354
- });
325
+ debugLog("stop-cleanup-sigterm", { error: e instanceof Error ? e.message : String(e) });
355
326
  }
356
- }
357
- clearCmuxSidebar(loadedPreferences);
358
- logCmuxEvent(loadedPreferences, `Auto-mode stopped${reasonSuffix || ""}.`, reason?.startsWith("Blocked:") ? "warning" : "info");
359
- if (isDebugEnabled()) {
360
- const logPath = writeDebugSummary();
361
- if (logPath) {
362
- ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
327
+ // ── Step 4: Auto-worktree exit ──
328
+ try {
329
+ if (s.currentMilestoneId) {
330
+ const notifyCtx = ctx
331
+ ? { notify: ctx.ui.notify.bind(ctx.ui) }
332
+ : { notify: () => { } };
333
+ buildResolver().exitMilestone(s.currentMilestoneId, notifyCtx, {
334
+ preserveBranch: true,
335
+ });
336
+ }
337
+ }
338
+ catch (e) {
339
+ debugLog("stop-cleanup-worktree", { error: e instanceof Error ? e.message : String(e) });
340
+ }
341
+ // ── Step 5: DB cleanup ──
342
+ if (isDbAvailable()) {
343
+ try {
344
+ const { closeDatabase } = await import("./gsd-db.js");
345
+ closeDatabase();
346
+ }
347
+ catch (e) {
348
+ debugLog("db-close-failed", {
349
+ error: e instanceof Error ? e.message : String(e),
350
+ });
351
+ }
352
+ }
353
+ // ── Step 6: Restore basePath and chdir ──
354
+ try {
355
+ if (s.originalBasePath) {
356
+ s.basePath = s.originalBasePath;
357
+ try {
358
+ process.chdir(s.basePath);
359
+ }
360
+ catch {
361
+ /* best-effort */
362
+ }
363
+ }
364
+ }
365
+ catch (e) {
366
+ debugLog("stop-cleanup-basepath", { error: e instanceof Error ? e.message : String(e) });
367
+ }
368
+ // ── Step 7: Ledger notification ──
369
+ try {
370
+ const ledger = getLedger();
371
+ if (ledger && ledger.units.length > 0) {
372
+ const totals = getProjectTotals(ledger.units);
373
+ ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}. Session: ${formatCost(totals.cost)} · ${formatTokenCount(totals.tokens.total)} tokens · ${ledger.units.length} units`, "info");
374
+ }
375
+ else {
376
+ ctx?.ui.notify(`Auto-mode stopped${reasonSuffix}.`, "info");
377
+ }
378
+ }
379
+ catch (e) {
380
+ debugLog("stop-cleanup-ledger", { error: e instanceof Error ? e.message : String(e) });
381
+ }
382
+ // ── Step 8: Rebuild state ──
383
+ if (s.basePath) {
384
+ try {
385
+ await rebuildState(s.basePath);
386
+ }
387
+ catch (e) {
388
+ debugLog("stop-rebuild-state-failed", {
389
+ error: e instanceof Error ? e.message : String(e),
390
+ });
391
+ }
392
+ }
393
+ // ── Step 9: Cmux sidebar / event log ──
394
+ try {
395
+ clearCmuxSidebar(loadedPreferences);
396
+ logCmuxEvent(loadedPreferences, `Auto-mode stopped${reasonSuffix || ""}.`, reason?.startsWith("Blocked:") ? "warning" : "info");
397
+ }
398
+ catch (e) {
399
+ debugLog("stop-cleanup-cmux", { error: e instanceof Error ? e.message : String(e) });
400
+ }
401
+ // ── Step 10: Debug summary ──
402
+ try {
403
+ if (isDebugEnabled()) {
404
+ const logPath = writeDebugSummary();
405
+ if (logPath) {
406
+ ctx?.ui.notify(`Debug log written → ${logPath}`, "info");
407
+ }
408
+ }
409
+ }
410
+ catch (e) {
411
+ debugLog("stop-cleanup-debug", { error: e instanceof Error ? e.message : String(e) });
412
+ }
413
+ // ── Step 11: Reset metrics, routing, hooks ──
414
+ try {
415
+ resetMetrics();
416
+ resetRoutingHistory();
417
+ resetHookState();
418
+ if (s.basePath)
419
+ clearPersistedHookState(s.basePath);
420
+ }
421
+ catch (e) {
422
+ debugLog("stop-cleanup-metrics", { error: e instanceof Error ? e.message : String(e) });
423
+ }
424
+ // ── Step 12: Remove paused-session metadata (#1383) ──
425
+ try {
426
+ const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
427
+ if (existsSync(pausedPath))
428
+ unlinkSync(pausedPath);
429
+ }
430
+ catch { /* non-fatal */ }
431
+ // ── Step 13: Restore original model (before reset clears IDs) ──
432
+ try {
433
+ if (pi && ctx && s.originalModelId && s.originalModelProvider) {
434
+ const original = ctx.modelRegistry.find(s.originalModelProvider, s.originalModelId);
435
+ if (original)
436
+ await pi.setModel(original);
437
+ }
438
+ }
439
+ catch (e) {
440
+ debugLog("stop-cleanup-model", { error: e instanceof Error ? e.message : String(e) });
363
441
  }
364
442
  }
365
- resetMetrics();
366
- resetRoutingHistory();
367
- resetHookState();
368
- if (s.basePath)
369
- clearPersistedHookState(s.basePath);
370
- // Remove paused-session metadata if present (#1383)
371
- try {
372
- const pausedPath = join(gsdRoot(s.originalBasePath || s.basePath), "runtime", "paused-session.json");
373
- if (existsSync(pausedPath))
374
- unlinkSync(pausedPath);
375
- }
376
- catch { /* non-fatal */ }
377
- s.active = false;
378
- s.paused = false;
379
- s.stepMode = false;
380
- s.unitDispatchCount.clear();
381
- s.unitRecoveryCount.clear();
382
- clearInFlightTools();
383
- s.lastBudgetAlertLevel = 0;
384
- s.lastStateRebuildAt = 0;
385
- s.unitLifetimeDispatches.clear();
386
- s.currentUnit = null;
387
- s.autoModeStartModel = null;
388
- s.currentMilestoneId = null;
389
- s.originalBasePath = "";
390
- s.completedUnits = [];
391
- s.pendingQuickTasks = [];
392
- clearSliceProgressCache();
393
- clearActivityLogState();
394
- resetProactiveHealing();
395
- s.pendingCrashRecovery = null;
396
- s.pendingVerificationRetry = null;
397
- s.verificationRetryCount.clear();
398
- s.pausedSessionFile = null;
399
- ctx?.ui.setStatus("gsd-auto", undefined);
400
- ctx?.ui.setWidget("gsd-progress", undefined);
401
- ctx?.ui.setFooter(undefined);
402
- if (pi && ctx && s.originalModelId && s.originalModelProvider) {
403
- const original = ctx.modelRegistry.find(s.originalModelProvider, s.originalModelId);
404
- if (original)
405
- await pi.setModel(original);
406
- s.originalModelId = null;
407
- s.originalModelProvider = null;
443
+ finally {
444
+ // ── Critical invariants: these MUST execute regardless of errors ──
445
+ // External cleanup (not covered by session reset)
446
+ clearInFlightTools();
447
+ clearSliceProgressCache();
448
+ clearActivityLogState();
449
+ resetProactiveHealing();
450
+ // UI cleanup
451
+ ctx?.ui.setStatus("gsd-auto", undefined);
452
+ ctx?.ui.setWidget("gsd-progress", undefined);
453
+ ctx?.ui.setFooter(undefined);
454
+ // Reset all session state in one call
455
+ s.reset();
408
456
  }
409
- s.cmdCtx = null;
410
457
  }
411
458
  /**
412
459
  * Pause auto-mode without destroying state. Context is preserved.
@@ -8,12 +8,13 @@
8
8
  import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
9
9
  import { dirname, join } from "node:path";
10
10
  import { homedir } from "node:os";
11
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
11
12
  // ─── Registry I/O ───────────────────────────────────────────────────────────
12
13
  function getRegistryPath() {
13
- return join(homedir(), ".gsd", "extensions", "registry.json");
14
+ return join(gsdHome, "extensions", "registry.json");
14
15
  }
15
16
  function getAgentExtensionsDir() {
16
- return join(homedir(), ".gsd", "agent", "extensions");
17
+ return join(gsdHome, "agent", "extensions");
17
18
  }
18
19
  function loadRegistry() {
19
20
  const filePath = getRegistryPath();
@@ -631,7 +631,7 @@ export function serializePreferencesToFrontmatter(prefs) {
631
631
  "dynamic_routing", "token_profile", "phases", "parallel",
632
632
  "auto_visualize", "auto_report",
633
633
  "verification_commands", "verification_auto_fix", "verification_max_retries",
634
- "search_provider", "compression_strategy", "context_selection",
634
+ "search_provider", "context_selection",
635
635
  ];
636
636
  const seen = new Set();
637
637
  for (const key of orderedKeys) {
@@ -3,10 +3,12 @@
3
3
  *
4
4
  * One command, one wizard. Routes to smart entry or status.
5
5
  */
6
+ import { importExtensionModule } from "@gsd/pi-coding-agent";
6
7
  import { existsSync, readFileSync, readdirSync, unlinkSync } from "node:fs";
7
8
  import { homedir } from "node:os";
8
9
  import { join } from "node:path";
9
10
  import { gsdRoot } from "./paths.js";
11
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
10
12
  import { enableDebug } from "./debug-logger.js";
11
13
  import { deriveState } from "./state.js";
12
14
  import { GSDDashboardOverlay } from "./dashboard-overlay.js";
@@ -437,7 +439,7 @@ export function registerGSDCommand(pi) {
437
439
  if (parts.length === 3 && ["enable", "disable", "info"].includes(parts[1])) {
438
440
  const idPrefix = parts[2] ?? "";
439
441
  try {
440
- const extDir = join(homedir(), ".gsd", "agent", "extensions");
442
+ const extDir = join(gsdHome, "agent", "extensions");
441
443
  const ids = [];
442
444
  for (const entry of readdirSync(extDir, { withFileTypes: true })) {
443
445
  if (!entry.isDirectory())
@@ -528,7 +530,7 @@ export async function handleGSDCommand(args, ctx, pi) {
528
530
  return;
529
531
  }
530
532
  if (trimmed === "widget" || trimmed.startsWith("widget ")) {
531
- const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await import("./auto-dashboard.js");
533
+ const { cycleWidgetMode, setWidgetMode, getWidgetMode } = await importExtensionModule(import.meta.url, "./auto-dashboard.js");
532
534
  const arg = trimmed.replace(/^widget\s*/, "").trim();
533
535
  if (arg === "full" || arg === "small" || arg === "min" || arg === "off") {
534
536
  setWidgetMode(arg);
@@ -8,7 +8,6 @@
8
8
  * @see D001 (module location), D002 (200K fallback), D003 (section-boundary truncation)
9
9
  */
10
10
  import { getCharsPerToken } from "./token-counter.js";
11
- import { compressToTarget } from "./prompt-compressor.js";
12
11
  // ─── Budget ratio constants ──────────────────────────────────────────────────
13
12
  // Percentages of total context window allocated to each budget category.
14
13
  // These are applied after tokens→chars conversion.
@@ -132,20 +131,13 @@ export function resolveExecutorContextWindow(registry, preferences, sessionConte
132
131
  return DEFAULT_CONTEXT_WINDOW;
133
132
  }
134
133
  /**
135
- * Smart context reduction: compress first, then truncate if still over budget.
136
- * Returns the content within budget with maximum information preservation.
134
+ * Reduce content to fit within budget using section-boundary truncation.
137
135
  */
138
136
  export function reduceToFit(content, budgetChars) {
139
137
  if (!content || content.length <= budgetChars) {
140
138
  return { content, droppedSections: 0 };
141
139
  }
142
- // Step 1: Try compression
143
- const compressed = compressToTarget(content, budgetChars);
144
- if (compressed.compressedChars <= budgetChars) {
145
- return { content: compressed.content, droppedSections: 0 };
146
- }
147
- // Step 2: Truncate the compressed content at section boundaries
148
- return truncateAtSectionBoundary(compressed.content, budgetChars);
140
+ return truncateAtSectionBoundary(content, budgetChars);
149
141
  }
150
142
  // ─── Internal helpers ────────────────────────────────────────────────────────
151
143
  /**
@@ -9,6 +9,7 @@ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
  import { homedir } from "node:os";
11
11
  import { gsdRoot } from "./paths.js";
12
+ const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
12
13
  // ─── Project File Markers ───────────────────────────────────────────────────────
13
14
  const PROJECT_FILES = [
14
15
  "package.json",
@@ -309,7 +310,6 @@ function detectVerificationCommands(basePath, detectedFiles, packageManager) {
309
310
  * Check if global GSD setup exists (has ~/.gsd/ with preferences).
310
311
  */
311
312
  export function hasGlobalSetup() {
312
- const gsdHome = join(homedir(), ".gsd");
313
313
  return (existsSync(join(gsdHome, "preferences.md")) ||
314
314
  existsSync(join(gsdHome, "PREFERENCES.md")));
315
315
  }
@@ -318,7 +318,6 @@ export function hasGlobalSetup() {
318
318
  * Returns true if ~/.gsd/ doesn't exist or has no preferences or auth.
319
319
  */
320
320
  export function isFirstEverLaunch() {
321
- const gsdHome = join(homedir(), ".gsd");
322
321
  if (!existsSync(gsdHome))
323
322
  return true;
324
323
  // If we have preferences, not first launch
@@ -194,8 +194,6 @@ Setting `prefer_skills: []` does **not** disable skill discovery — it just mea
194
194
 
195
195
  - `search_provider`: `"brave"`, `"tavily"`, `"ollama"`, `"native"`, or `"auto"` — selects the search backend for research phases. `"native"` forces Anthropic's built-in web search only; provider values force that backend and disable native search; `"auto"` uses the default heuristic. Default: `"auto"`.
196
196
 
197
- - `compression_strategy`: `"truncate"` or `"compress"` — controls how context that exceeds the budget is reduced. `"truncate"` (default) drops sections from the end. `"compress"` applies heuristic compression before truncating, preserving more content at the cost of some fidelity. Default: `"truncate"`.
198
-
199
197
  - `context_selection`: `"full"` or `"smart"` — controls how files are inlined into context. `"full"` inlines entire files; `"smart"` uses semantic chunking to include only the most relevant sections. Default is derived from `token_profile`.
200
198
 
201
199
  - `parallel`: configures parallel orchestration for running multiple slices concurrently. Keys:
@@ -28,10 +28,12 @@ function modelToProviderId(model) {
28
28
  const prefix = model.split("/")[0].toLowerCase();
29
29
  // Map known prefixes to registry IDs
30
30
  const prefixMap = {
31
+ "anthropic-vertex": "anthropic-vertex",
31
32
  openrouter: "openrouter",
32
33
  groq: "groq",
33
34
  mistral: "mistral",
34
35
  google: "google",
36
+ "google-vertex": "google-vertex",
35
37
  anthropic: "anthropic",
36
38
  openai: "openai",
37
39
  "github-copilot": "github-copilot",
@@ -67,11 +69,19 @@ function collectConfiguredModelProviders() {
67
69
  }
68
70
  const modelEntries = typeof models === "object" ? Object.values(models) : [];
69
71
  for (const entry of modelEntries) {
70
- const modelId = typeof entry === "string" ? entry
71
- : typeof entry === "object" && entry !== null && "model" in entry
72
- ? String(entry.model)
73
- : null;
74
- if (modelId) {
72
+ if (typeof entry === "string") {
73
+ const pid = modelToProviderId(entry);
74
+ if (pid)
75
+ providers.add(pid);
76
+ continue;
77
+ }
78
+ if (typeof entry === "object" && entry !== null && "model" in entry) {
79
+ const configuredProvider = "provider" in entry ? entry.provider : undefined;
80
+ if (typeof configuredProvider === "string" && configuredProvider.trim().length > 0) {
81
+ providers.add(configuredProvider);
82
+ continue;
83
+ }
84
+ const modelId = String(entry.model);
75
85
  const pid = modelToProviderId(modelId);
76
86
  if (pid)
77
87
  providers.add(pid);
@@ -88,6 +98,9 @@ function collectConfiguredModelProviders() {
88
98
  }
89
99
  function resolveKey(providerId) {
90
100
  const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
101
+ if (providerId === "anthropic-vertex" && process.env.ANTHROPIC_VERTEX_PROJECT_ID) {
102
+ return { found: true, source: "env", backedOff: false };
103
+ }
91
104
  // Check auth.json
92
105
  const authPath = getAuthPath();
93
106
  if (existsSync(authPath)) {
@@ -138,7 +151,9 @@ function checkLlmProviders() {
138
151
  const results = [];
139
152
  for (const providerId of required) {
140
153
  const info = PROVIDER_REGISTRY.find(p => p.id === providerId);
141
- const label = info?.label ?? providerId;
154
+ const label = providerId === "anthropic-vertex"
155
+ ? "Anthropic Vertex"
156
+ : info?.label ?? providerId;
142
157
  const lookup = resolveKey(providerId);
143
158
  if (!lookup.found) {
144
159
  // Check if a cross-provider can serve this provider's models
@@ -157,16 +172,20 @@ function checkLlmProviders() {
157
172
  });
158
173
  continue;
159
174
  }
160
- const envVar = info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
175
+ const envVar = providerId === "anthropic-vertex"
176
+ ? "ANTHROPIC_VERTEX_PROJECT_ID"
177
+ : info?.envVar ?? `${providerId.toUpperCase()}_API_KEY`;
161
178
  results.push({
162
179
  name: providerId,
163
180
  label,
164
181
  category: "llm",
165
182
  status: "error",
166
- message: `${label} — no API key found`,
167
- detail: info?.hasOAuth
168
- ? `Run /gsd keys to authenticate`
169
- : `Set ${envVar} or run /gsd keys`,
183
+ message: `${label} — not configured`,
184
+ detail: providerId === "anthropic-vertex"
185
+ ? "Set ANTHROPIC_VERTEX_PROJECT_ID and authenticate with Google ADC"
186
+ : info?.hasOAuth
187
+ ? `Run /gsd keys to authenticate`
188
+ : `Set ${envVar} or run /gsd keys`,
170
189
  required: true,
171
190
  });
172
191
  }
@@ -257,10 +257,23 @@ async function markSliceDoneInRoadmap(basePath, milestoneId, sliceId, fixesAppli
257
257
  fixesApplied.push(`marked ${sliceId} done in ${roadmapPath}`);
258
258
  }
259
259
  }
260
+ async function markSliceUndoneInRoadmap(basePath, milestoneId, sliceId, fixesApplied) {
261
+ const roadmapPath = resolveMilestoneFile(basePath, milestoneId, "ROADMAP");
262
+ if (!roadmapPath)
263
+ return;
264
+ const content = await loadFile(roadmapPath);
265
+ if (!content)
266
+ return;
267
+ const updated = content.replace(new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sliceId}:`, "m"), `$1[ ] **${sliceId}:`);
268
+ if (updated !== content) {
269
+ await saveFile(roadmapPath, updated);
270
+ fixesApplied.push(`unmarked ${sliceId} in ${roadmapPath} (premature completion)`);
271
+ }
272
+ }
260
273
  function matchesScope(unitId, scope) {
261
274
  if (!scope)
262
275
  return true;
263
- return unitId === scope || unitId.startsWith(`${scope}/`) || unitId.startsWith(`${scope}`);
276
+ return unitId === scope || unitId.startsWith(`${scope}/`);
264
277
  }
265
278
  function auditRequirements(content) {
266
279
  if (!content)
@@ -828,6 +841,12 @@ export async function runGSDDoctor(basePath, options) {
828
841
  file: relSliceFile(basePath, milestoneId, slice.id, "SUMMARY"),
829
842
  fixable: true,
830
843
  });
844
+ if (!allTasksDone) {
845
+ dryRunCanFix("slice_checked_missing_summary", `uncheck ${slice.id} in roadmap (tasks incomplete)`);
846
+ if (shouldFix("slice_checked_missing_summary")) {
847
+ await markSliceUndoneInRoadmap(basePath, milestoneId, slice.id, fixesApplied);
848
+ }
849
+ }
831
850
  }
832
851
  if (slice.done && !hasSliceUat) {
833
852
  issues.push({
@@ -1,9 +1,10 @@
1
+ import { importExtensionModule } from "@gsd/pi-coding-agent";
1
2
  export function registerExitCommand(pi, deps = {}) {
2
3
  pi.registerCommand("exit", {
3
4
  description: "Exit GSD gracefully",
4
5
  handler: async (_args, ctx) => {
5
6
  // Stop auto-mode first so locks and activity state are cleaned up before shutdown.
6
- const stopAuto = deps.stopAuto ?? (await import("./auto.js")).stopAuto;
7
+ const stopAuto = deps.stopAuto ?? (await importExtensionModule(import.meta.url, "./auto.js")).stopAuto;
7
8
  await stopAuto(ctx, pi, "Graceful exit");
8
9
  ctx.shutdown();
9
10
  },
@@ -5,7 +5,7 @@ import { join, basename } from "node:path";
5
5
  import { exec } from "node:child_process";
6
6
  import { getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice, aggregateByModel, formatCost, formatTokenCount, loadLedgerFromDisk, } from "./metrics.js";
7
7
  import { gsdRoot } from "./paths.js";
8
- import { formatDuration, fileLink } from "../shared/mod.js";
8
+ import { formatDuration, fileLink } from "../shared/format-utils.js";
9
9
  import { getErrorMessage } from "./error-utils.js";
10
10
  /**
11
11
  * Open a file in the user's default browser.
@@ -6,8 +6,8 @@ import { promises as fs } from 'node:fs';
6
6
  import { resolve } from 'node:path';
7
7
  import { atomicWriteAsync } from './atomic-write.js';
8
8
  import { resolveMilestoneFile, relMilestoneFile, resolveGsdRootFile } from './paths.js';
9
- import { findMilestoneIds } from './guided-flow.js';
10
- import { checkExistingEnvKeys } from '../get-secrets-from-user.js';
9
+ import { findMilestoneIds } from './milestone-ids.js';
10
+ import { checkExistingEnvKeys } from '../env-utils.js';
11
11
  import { parseRoadmapSlices } from './roadmap-slices.js';
12
12
  import { nativeParseRoadmap, nativeExtractSection, nativeParsePlanFile, nativeParseSummaryFile, NATIVE_UNAVAILABLE } from './native-parser-bridge.js';
13
13
  import { debugTime, debugCount } from './debug-logger.js';
@@ -222,13 +222,48 @@ export function formatSecretsManifest(manifest) {
222
222
  return lines.join('\n') + '\n';
223
223
  }
224
224
  // ─── Slice Plan Parser ─────────────────────────────────────────────────────
225
+ function normalizeTaskPlanFrontmatter(frontmatter) {
226
+ const estimatedStepsRaw = frontmatter.estimated_steps;
227
+ const estimatedFilesRaw = frontmatter.estimated_files;
228
+ const skillsUsedRaw = frontmatter.skills_used;
229
+ const parseOptionalNumber = (value) => {
230
+ if (typeof value === 'number' && Number.isFinite(value))
231
+ return value;
232
+ if (typeof value === 'string' && value.trim()) {
233
+ const parsed = parseInt(value, 10);
234
+ if (Number.isFinite(parsed))
235
+ return parsed;
236
+ }
237
+ return undefined;
238
+ };
239
+ const estimated_steps = parseOptionalNumber(estimatedStepsRaw);
240
+ const estimated_files = parseOptionalNumber(estimatedFilesRaw);
241
+ const skills_used = Array.isArray(skillsUsedRaw)
242
+ ? skillsUsedRaw.map(v => String(v).trim()).filter(Boolean)
243
+ : typeof skillsUsedRaw === 'string' && skillsUsedRaw.trim()
244
+ ? [skillsUsedRaw.trim()]
245
+ : [];
246
+ return {
247
+ ...(estimated_steps !== undefined ? { estimated_steps } : {}),
248
+ ...(estimated_files !== undefined ? { estimated_files } : {}),
249
+ skills_used,
250
+ };
251
+ }
252
+ export function parseTaskPlanFile(content) {
253
+ const [fmLines] = splitFrontmatter(content);
254
+ const fm = fmLines ? parseFrontmatterMap(fmLines) : {};
255
+ return {
256
+ frontmatter: normalizeTaskPlanFrontmatter(fm),
257
+ };
258
+ }
225
259
  export function parsePlan(content) {
226
260
  return cachedParse(content, 'plan', _parsePlanImpl);
227
261
  }
228
262
  function _parsePlanImpl(content) {
229
263
  const stopTimer = debugTime("parse-plan");
264
+ const [, body] = splitFrontmatter(content);
230
265
  // Try native parser first for better performance
231
- const nativeResult = nativeParsePlanFile(content);
266
+ const nativeResult = nativeParsePlanFile(body);
232
267
  if (nativeResult) {
233
268
  stopTimer({ native: true });
234
269
  return {
@@ -249,7 +284,7 @@ function _parsePlanImpl(content) {
249
284
  filesLikelyTouched: nativeResult.filesLikelyTouched,
250
285
  };
251
286
  }
252
- const lines = content.split('\n');
287
+ const lines = body.split('\n');
253
288
  const h1 = lines.find(l => l.startsWith('# '));
254
289
  let id = '';
255
290
  let title = '';
@@ -263,11 +298,11 @@ function _parsePlanImpl(content) {
263
298
  title = h1.slice(2).trim();
264
299
  }
265
300
  }
266
- const goal = extractBoldField(content, 'Goal') || '';
267
- const demo = extractBoldField(content, 'Demo') || '';
268
- const mhSection = extractSection(content, 'Must-Haves');
301
+ const goal = extractBoldField(body, 'Goal') || '';
302
+ const demo = extractBoldField(body, 'Demo') || '';
303
+ const mhSection = extractSection(body, 'Must-Haves');
269
304
  const mustHaves = mhSection ? parseBullets(mhSection) : [];
270
- const tasksSection = extractSection(content, 'Tasks');
305
+ const tasksSection = extractSection(body, 'Tasks');
271
306
  const tasks = [];
272
307
  if (tasksSection) {
273
308
  const taskLines = tasksSection.split('\n');
@@ -315,7 +350,7 @@ function _parsePlanImpl(content) {
315
350
  if (currentTask)
316
351
  tasks.push(currentTask);
317
352
  }
318
- const filesSection = extractSection(content, 'Files Likely Touched');
353
+ const filesSection = extractSection(body, 'Files Likely Touched');
319
354
  const filesLikelyTouched = filesSection ? parseBullets(filesSection) : [];
320
355
  const result = { id, title, goal, demo, mustHaves, tasks, filesLikelyTouched };
321
356
  stopTimer({ tasks: tasks.length });
@@ -692,6 +727,10 @@ export function extractUatType(content) {
692
727
  const rawValue = modeBullet.slice('UAT mode:'.length).trim().toLowerCase();
693
728
  if (rawValue.startsWith('artifact-driven'))
694
729
  return 'artifact-driven';
730
+ if (rawValue.startsWith('browser-executable'))
731
+ return 'browser-executable';
732
+ if (rawValue.startsWith('runtime-executable'))
733
+ return 'runtime-executable';
695
734
  if (rawValue.startsWith('live-runtime'))
696
735
  return 'live-runtime';
697
736
  if (rawValue.startsWith('human-experience'))
@@ -21,7 +21,7 @@ import { deriveState } from "./state.js";
21
21
  import { isAutoActive } from "./auto.js";
22
22
  import { loadPrompt } from "./prompt-loader.js";
23
23
  import { gsdRoot } from "./paths.js";
24
- import { formatDuration } from "../shared/mod.js";
24
+ import { formatDuration } from "../shared/format-utils.js";
25
25
  import { getAutoWorktreePath } from "./auto-worktree.js";
26
26
  // ─── Entry Point ──────────────────────────────────────────────────────────────
27
27
  export async function handleForensics(args, ctx, pi) {