gsd-pi 2.39.0 → 2.40.0-dev.4a93031

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 (133) hide show
  1. package/dist/resource-loader.js +66 -2
  2. package/dist/resources/extensions/async-jobs/index.js +10 -0
  3. package/dist/resources/extensions/get-secrets-from-user.js +1 -1
  4. package/dist/resources/extensions/gsd/auto-dashboard.js +7 -0
  5. package/dist/resources/extensions/gsd/auto-loop.js +761 -673
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +10 -2
  7. package/dist/resources/extensions/gsd/auto-prompts.js +3 -3
  8. package/dist/resources/extensions/gsd/auto-start.js +6 -1
  9. package/dist/resources/extensions/gsd/auto.js +6 -4
  10. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +126 -0
  11. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +233 -0
  12. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +59 -0
  13. package/dist/resources/extensions/gsd/bootstrap/register-extension.js +38 -0
  14. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +156 -0
  15. package/dist/resources/extensions/gsd/bootstrap/register-shortcuts.js +46 -0
  16. package/dist/resources/extensions/gsd/bootstrap/system-context.js +300 -0
  17. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +38 -0
  18. package/dist/resources/extensions/gsd/commands/catalog.js +278 -0
  19. package/dist/resources/extensions/gsd/commands/context.js +84 -0
  20. package/dist/resources/extensions/gsd/commands/dispatcher.js +21 -0
  21. package/dist/resources/extensions/gsd/commands/handlers/auto.js +72 -0
  22. package/dist/resources/extensions/gsd/commands/handlers/core.js +246 -0
  23. package/dist/resources/extensions/gsd/commands/handlers/ops.js +166 -0
  24. package/dist/resources/extensions/gsd/commands/handlers/parallel.js +94 -0
  25. package/dist/resources/extensions/gsd/commands/handlers/workflow.js +102 -0
  26. package/dist/resources/extensions/gsd/commands/index.js +11 -0
  27. package/dist/resources/extensions/gsd/commands-handlers.js +1 -1
  28. package/dist/resources/extensions/gsd/commands.js +8 -1190
  29. package/dist/resources/extensions/gsd/dashboard-overlay.js +9 -0
  30. package/dist/resources/extensions/gsd/doctor-proactive.js +80 -10
  31. package/dist/resources/extensions/gsd/doctor.js +32 -2
  32. package/dist/resources/extensions/gsd/export-html.js +46 -0
  33. package/dist/resources/extensions/gsd/files.js +1 -1
  34. package/dist/resources/extensions/gsd/health-widget.js +1 -1
  35. package/dist/resources/extensions/gsd/index.js +4 -1115
  36. package/dist/resources/extensions/gsd/progress-score.js +20 -1
  37. package/dist/resources/extensions/gsd/prompts/forensics.md +121 -46
  38. package/dist/resources/extensions/gsd/visualizer-data.js +26 -1
  39. package/dist/resources/extensions/gsd/visualizer-views.js +52 -0
  40. package/dist/welcome-screen.d.ts +3 -2
  41. package/dist/welcome-screen.js +66 -22
  42. package/package.json +1 -1
  43. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
  44. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  45. package/packages/pi-coding-agent/dist/core/agent-session.js +107 -24
  46. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  47. package/packages/pi-coding-agent/dist/core/skill-tool.test.d.ts +2 -0
  48. package/packages/pi-coding-agent/dist/core/skill-tool.test.d.ts.map +1 -0
  49. package/packages/pi-coding-agent/dist/core/skill-tool.test.js +70 -0
  50. package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -0
  51. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/skills.js +2 -1
  53. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts +17 -0
  55. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -0
  56. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +244 -0
  57. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -0
  58. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts +3 -0
  59. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.d.ts.map +1 -0
  60. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js +58 -0
  61. package/packages/pi-coding-agent/dist/modes/interactive/controllers/extension-ui-controller.js.map +1 -0
  62. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts +12 -0
  63. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.d.ts.map +1 -0
  64. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js +54 -0
  65. package/packages/pi-coding-agent/dist/modes/interactive/controllers/input-controller.js.map +1 -0
  66. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts +6 -0
  67. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.d.ts.map +1 -0
  68. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js +63 -0
  69. package/packages/pi-coding-agent/dist/modes/interactive/controllers/model-controller.js.map +1 -0
  70. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts +38 -0
  71. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.d.ts.map +1 -0
  72. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js +2 -0
  73. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-state.js.map +1 -0
  74. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +1 -1
  75. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  76. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +15 -457
  77. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  78. package/packages/pi-coding-agent/package.json +1 -1
  79. package/packages/pi-coding-agent/src/core/agent-session.ts +122 -23
  80. package/packages/pi-coding-agent/src/core/skill-tool.test.ts +89 -0
  81. package/packages/pi-coding-agent/src/core/skills.ts +2 -1
  82. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +302 -0
  83. package/packages/pi-coding-agent/src/modes/interactive/controllers/extension-ui-controller.ts +59 -0
  84. package/packages/pi-coding-agent/src/modes/interactive/controllers/input-controller.ts +68 -0
  85. package/packages/pi-coding-agent/src/modes/interactive/controllers/model-controller.ts +71 -0
  86. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-state.ts +37 -0
  87. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +18 -510
  88. package/pkg/package.json +1 -1
  89. package/src/resources/extensions/async-jobs/index.ts +11 -0
  90. package/src/resources/extensions/get-secrets-from-user.ts +1 -1
  91. package/src/resources/extensions/gsd/auto-dashboard.ts +10 -0
  92. package/src/resources/extensions/gsd/auto-loop.ts +1075 -921
  93. package/src/resources/extensions/gsd/auto-post-unit.ts +10 -2
  94. package/src/resources/extensions/gsd/auto-prompts.ts +3 -3
  95. package/src/resources/extensions/gsd/auto-start.ts +6 -1
  96. package/src/resources/extensions/gsd/auto.ts +13 -10
  97. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +142 -0
  98. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +238 -0
  99. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +90 -0
  100. package/src/resources/extensions/gsd/bootstrap/register-extension.ts +46 -0
  101. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +167 -0
  102. package/src/resources/extensions/gsd/bootstrap/register-shortcuts.ts +55 -0
  103. package/src/resources/extensions/gsd/bootstrap/system-context.ts +340 -0
  104. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +51 -0
  105. package/src/resources/extensions/gsd/commands/catalog.ts +301 -0
  106. package/src/resources/extensions/gsd/commands/context.ts +101 -0
  107. package/src/resources/extensions/gsd/commands/dispatcher.ts +32 -0
  108. package/src/resources/extensions/gsd/commands/handlers/auto.ts +74 -0
  109. package/src/resources/extensions/gsd/commands/handlers/core.ts +274 -0
  110. package/src/resources/extensions/gsd/commands/handlers/ops.ts +169 -0
  111. package/src/resources/extensions/gsd/commands/handlers/parallel.ts +118 -0
  112. package/src/resources/extensions/gsd/commands/handlers/workflow.ts +109 -0
  113. package/src/resources/extensions/gsd/commands/index.ts +14 -0
  114. package/src/resources/extensions/gsd/commands-handlers.ts +1 -1
  115. package/src/resources/extensions/gsd/commands.ts +10 -1329
  116. package/src/resources/extensions/gsd/dashboard-overlay.ts +10 -0
  117. package/src/resources/extensions/gsd/doctor-proactive.ts +106 -10
  118. package/src/resources/extensions/gsd/doctor.ts +47 -3
  119. package/src/resources/extensions/gsd/export-html.ts +51 -0
  120. package/src/resources/extensions/gsd/files.ts +1 -1
  121. package/src/resources/extensions/gsd/health-widget.ts +2 -1
  122. package/src/resources/extensions/gsd/index.ts +12 -1314
  123. package/src/resources/extensions/gsd/progress-score.ts +23 -0
  124. package/src/resources/extensions/gsd/prompts/forensics.md +121 -46
  125. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +13 -9
  126. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +3 -3
  127. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +16 -16
  128. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +4 -4
  129. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +10 -10
  130. package/src/resources/extensions/gsd/visualizer-data.ts +51 -1
  131. package/src/resources/extensions/gsd/visualizer-views.ts +58 -0
  132. /package/dist/resources/extensions/{env-utils.js → gsd/env-utils.js} +0 -0
  133. /package/src/resources/extensions/{env-utils.ts → gsd/env-utils.ts} +0 -0
@@ -168,8 +168,12 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
168
168
  const sliceTerminalUnits = new Set(["complete-slice", "run-uat"]);
169
169
  const effectiveFixLevel = sliceTerminalUnits.has(s.currentUnit.type) ? "all" as const : "task" as const;
170
170
  const report = await runGSDDoctor(s.basePath, { fix: true, scope: doctorScope, fixLevel: effectiveFixLevel });
171
+ // Human-readable fix notification with details
171
172
  if (report.fixesApplied.length > 0) {
172
- ctx.ui.notify(`Post-hook: applied ${report.fixesApplied.length} fix(es).`, "info");
173
+ const fixSummary = report.fixesApplied.length <= 2
174
+ ? report.fixesApplied.join("; ")
175
+ : `${report.fixesApplied[0]}; +${report.fixesApplied.length - 1} more`;
176
+ ctx.ui.notify(`Doctor: ${fixSummary}`, "info");
173
177
  }
174
178
 
175
179
  // Proactive health tracking — filter to current milestone to avoid
@@ -181,7 +185,11 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
181
185
  i.unitId.startsWith(`${currentMilestoneId}/`))
182
186
  : report.issues;
183
187
  const summary = summarizeDoctorIssues(milestoneIssues);
184
- recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length);
188
+ // Pass issue details + scope for real-time visibility in the progress widget
189
+ const issueDetails = milestoneIssues
190
+ .filter(i => i.severity === "error" || i.severity === "warning")
191
+ .map(i => ({ code: i.code, message: i.message, severity: i.severity, unitId: i.unitId }));
192
+ recordHealthSnapshot(summary.errors, summary.warnings, report.fixesApplied.length, issueDetails, report.fixesApplied, doctorScope);
185
193
 
186
194
  // Check if we should escalate to LLM-assisted heal
187
195
  if (summary.errors > 0) {
@@ -759,8 +759,8 @@ export async function checkNeedsRunUat(
759
759
  if (hasResult) return null;
760
760
  }
761
761
 
762
- // Classify UAT type; unknown type treat as human-experience (human review)
763
- const uatType = extractUatType(uatContent) ?? "human-experience";
762
+ // Classify UAT type; default to artifact-driven (LLM-executed UATs are always artifact-driven)
763
+ const uatType = extractUatType(uatContent) ?? "artifact-driven";
764
764
 
765
765
  return { sliceId: sid, uatType };
766
766
  }
@@ -1403,7 +1403,7 @@ export async function buildRunUatPrompt(
1403
1403
  const inlinedContext = capPreamble(`## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`);
1404
1404
 
1405
1405
  const uatResultPath = join(base, relSliceFile(base, mid, sliceId, "UAT-RESULT"));
1406
- const uatType = extractUatType(uatContent) ?? "human-experience";
1406
+ const uatType = extractUatType(uatContent) ?? "artifact-driven";
1407
1407
 
1408
1408
  return loadPrompt("run-uat", {
1409
1409
  workingDirectory: base,
@@ -56,7 +56,7 @@ import { readResourceVersion } from "./auto-worktree-sync.js";
56
56
  import { initMetrics } from "./metrics.js";
57
57
  import { initRoutingHistory } from "./routing-history.js";
58
58
  import { restoreHookState, resetHookState } from "./post-unit-hooks.js";
59
- import { resetProactiveHealing } from "./doctor-proactive.js";
59
+ import { resetProactiveHealing, setLevelChangeCallback } from "./doctor-proactive.js";
60
60
  import { snapshotSkills } from "./skill-discovery.js";
61
61
  import { isDbAvailable } from "./gsd-db.js";
62
62
  import { hideFooter } from "./auto-dashboard.js";
@@ -415,6 +415,11 @@ export async function bootstrapAutoSession(
415
415
  resetHookState();
416
416
  restoreHookState(base);
417
417
  resetProactiveHealing();
418
+ // Notify user on health level transitions (green→yellow→red and back)
419
+ setLevelChangeCallback((_from, to, summary) => {
420
+ const level = to === "red" ? "error" : to === "yellow" ? "warning" : "info";
421
+ ctx.ui.notify(summary, level as "info" | "warning" | "error");
422
+ });
418
423
  s.autoStartTime = Date.now();
419
424
  s.resourceVersionOnStart = readResourceVersion();
420
425
  s.completedUnits = [];
@@ -111,6 +111,7 @@ import {
111
111
  recordHealthSnapshot,
112
112
  checkHealEscalation,
113
113
  resetProactiveHealing,
114
+ setLevelChangeCallback,
114
115
  formatHealthSummary,
115
116
  getConsecutiveErrorUnits,
116
117
  } from "./doctor-proactive.js";
@@ -195,7 +196,7 @@ import {
195
196
  postUnitPostVerification,
196
197
  } from "./auto-post-unit.js";
197
198
  import { bootstrapAutoSession, type BootstrapDeps } from "./auto-start.js";
198
- import { autoLoop, resolveAgentEnd, type LoopDeps } from "./auto-loop.js";
199
+ import { autoLoop, resolveAgentEnd, isSessionSwitchInFlight, type LoopDeps } from "./auto-loop.js";
199
200
  import {
200
201
  WorktreeResolver,
201
202
  type WorktreeResolverDeps,
@@ -687,6 +688,7 @@ export async function stopAuto(
687
688
  clearInFlightTools();
688
689
  clearSliceProgressCache();
689
690
  clearActivityLogState();
691
+ setLevelChangeCallback(null);
690
692
  resetProactiveHealing();
691
693
 
692
694
  // UI cleanup
@@ -1129,6 +1131,7 @@ const widgetStateAccessors: WidgetStateAccessors = {
1129
1131
  getCmdCtx: () => s.cmdCtx,
1130
1132
  getBasePath: () => s.basePath,
1131
1133
  isVerbose: () => s.verbose,
1134
+ isSessionSwitching: isSessionSwitchInFlight,
1132
1135
  };
1133
1136
 
1134
1137
  // ─── Preconditions ────────────────────────────────────────────────────────────
@@ -1183,15 +1186,6 @@ function buildRecoveryContext(): import("./auto-timeout-recovery.js").RecoveryCo
1183
1186
  };
1184
1187
  }
1185
1188
 
1186
- // Re-export recovery functions for external consumers
1187
- export {
1188
- resolveExpectedArtifactPath,
1189
- verifyExpectedArtifact,
1190
- writeBlockerPlaceholder,
1191
- skipExecuteTask,
1192
- buildLoopRemediationSteps,
1193
- } from "./auto-recovery.js";
1194
-
1195
1189
  /**
1196
1190
  * Test-only: expose skip-loop state for unit tests.
1197
1191
  * Not part of the public API.
@@ -1327,3 +1321,12 @@ export async function dispatchHookUnit(
1327
1321
 
1328
1322
  // Direct phase dispatch → auto-direct-dispatch.ts
1329
1323
  export { dispatchDirectPhase } from "./auto-direct-dispatch.js";
1324
+
1325
+ // Re-export recovery functions for external consumers
1326
+ export {
1327
+ resolveExpectedArtifactPath,
1328
+ verifyExpectedArtifact,
1329
+ writeBlockerPlaceholder,
1330
+ skipExecuteTask,
1331
+ buildLoopRemediationSteps,
1332
+ } from "./auto-recovery.js";
@@ -0,0 +1,142 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
2
+
3
+ import { checkAutoStartAfterDiscuss } from "../guided-flow.js";
4
+ import { getAutoDashboardData, getAutoModeStartModel, isAutoActive, pauseAuto } from "../auto.js";
5
+ import { getNextFallbackModel, isTransientNetworkError, resolveModelWithFallbacksForUnit } from "../preferences.js";
6
+ import { classifyProviderError, pauseAutoForProviderError } from "../provider-error-pause.js";
7
+ import { isSessionSwitchInFlight, resolveAgentEnd } from "../auto-loop.js";
8
+ import { clearDiscussionFlowState } from "./write-gate.js";
9
+
10
+ const networkRetryCounters = new Map<string, number>();
11
+ const MAX_TRANSIENT_AUTO_RESUMES = 3;
12
+ let consecutiveTransientErrors = 0;
13
+
14
+ export async function handleAgentEnd(
15
+ pi: ExtensionAPI,
16
+ event: { messages: any[] },
17
+ ctx: ExtensionContext,
18
+ ): Promise<void> {
19
+ if (checkAutoStartAfterDiscuss()) {
20
+ clearDiscussionFlowState();
21
+ return;
22
+ }
23
+ if (!isAutoActive()) return;
24
+ if (isSessionSwitchInFlight()) return;
25
+
26
+ const lastMsg = event.messages[event.messages.length - 1];
27
+ if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
28
+ await pauseAuto(ctx, pi);
29
+ return;
30
+ }
31
+ if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "error") {
32
+ const errorDetail = "errorMessage" in lastMsg && lastMsg.errorMessage ? `: ${lastMsg.errorMessage}` : "";
33
+ const errorMsg = ("errorMessage" in lastMsg && lastMsg.errorMessage) ? String(lastMsg.errorMessage) : "";
34
+
35
+ if (isTransientNetworkError(errorMsg)) {
36
+ const currentModelId = ctx.model?.id ?? "unknown";
37
+ const retryKey = `network-retry:${currentModelId}`;
38
+ const currentRetries = networkRetryCounters.get(retryKey) ?? 0;
39
+ const maxRetries = 2;
40
+ if (currentRetries < maxRetries) {
41
+ networkRetryCounters.set(retryKey, currentRetries + 1);
42
+ const attempt = currentRetries + 1;
43
+ const delayMs = attempt * 3000;
44
+ ctx.ui.notify(`Network error on ${currentModelId}${errorDetail}. Retry ${attempt}/${maxRetries} in ${delayMs / 1000}s...`, "warning");
45
+ setTimeout(() => {
46
+ pi.sendMessage(
47
+ { customType: "gsd-auto-timeout-recovery", content: "Continue execution — retrying after transient network error.", display: false },
48
+ { triggerTurn: true },
49
+ );
50
+ }, delayMs);
51
+ return;
52
+ }
53
+ networkRetryCounters.delete(retryKey);
54
+ ctx.ui.notify(`Network retries exhausted for ${currentModelId}. Attempting model fallback.`, "warning");
55
+ }
56
+
57
+ const dash = getAutoDashboardData();
58
+ if (dash.currentUnit) {
59
+ const modelConfig = resolveModelWithFallbacksForUnit(dash.currentUnit.type);
60
+ if (modelConfig && modelConfig.fallbacks.length > 0) {
61
+ const availableModels = ctx.modelRegistry.getAvailable();
62
+ const nextModelId = getNextFallbackModel(ctx.model?.id, modelConfig);
63
+ if (nextModelId) {
64
+ networkRetryCounters.clear();
65
+ const slashIdx = nextModelId.indexOf("/");
66
+ const modelToSet = slashIdx !== -1
67
+ ? availableModels.find((m) => m.provider.toLowerCase() === nextModelId.substring(0, slashIdx).toLowerCase() && m.id.toLowerCase() === nextModelId.substring(slashIdx + 1).toLowerCase())
68
+ : (availableModels.find((m) => m.id === nextModelId && m.provider === ctx.model?.provider) ?? availableModels.find((m) => m.id === nextModelId));
69
+ if (modelToSet) {
70
+ const ok = await pi.setModel(modelToSet, { persist: false });
71
+ if (ok) {
72
+ ctx.ui.notify(`Model error${errorDetail}. Switched to fallback: ${nextModelId} and resuming.`, "warning");
73
+ pi.sendMessage({ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
74
+ return;
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ const sessionModel = getAutoModeStartModel();
82
+ if (sessionModel) {
83
+ if (ctx.model?.id !== sessionModel.id || ctx.model?.provider !== sessionModel.provider) {
84
+ const startModel = ctx.modelRegistry.getAvailable().find((m) => m.provider === sessionModel.provider && m.id === sessionModel.id);
85
+ if (startModel) {
86
+ const ok = await pi.setModel(startModel, { persist: false });
87
+ if (ok) {
88
+ networkRetryCounters.clear();
89
+ ctx.ui.notify(`Model error${errorDetail}. Restored session model: ${sessionModel.provider}/${sessionModel.id} and resuming.`, "warning");
90
+ pi.sendMessage({ customType: "gsd-auto-timeout-recovery", content: "Continue execution.", display: false }, { triggerTurn: true });
91
+ return;
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ const classification = classifyProviderError(errorMsg);
98
+ const explicitRetryAfterMs = ("retryAfterMs" in lastMsg && typeof lastMsg.retryAfterMs === "number") ? lastMsg.retryAfterMs : undefined;
99
+ if (classification.isTransient) {
100
+ consecutiveTransientErrors += 1;
101
+ } else {
102
+ consecutiveTransientErrors = 0;
103
+ }
104
+ const baseRetryAfterMs = explicitRetryAfterMs ?? classification.suggestedDelayMs;
105
+ const retryAfterMs = classification.isTransient
106
+ ? baseRetryAfterMs * 2 ** Math.max(0, consecutiveTransientErrors - 1)
107
+ : baseRetryAfterMs;
108
+ const allowAutoResume = classification.isTransient && consecutiveTransientErrors <= MAX_TRANSIENT_AUTO_RESUMES;
109
+ if (classification.isTransient && !allowAutoResume) {
110
+ ctx.ui.notify(`Transient provider errors persisted after ${MAX_TRANSIENT_AUTO_RESUMES} auto-resume attempts. Pausing for manual review.`, "warning");
111
+ }
112
+ await pauseAutoForProviderError(ctx.ui, errorDetail, () => pauseAuto(ctx, pi), {
113
+ isRateLimit: classification.isRateLimit,
114
+ isTransient: allowAutoResume,
115
+ retryAfterMs,
116
+ resume: allowAutoResume
117
+ ? () => {
118
+ pi.sendMessage(
119
+ { customType: "gsd-auto-timeout-recovery", content: "Continue execution — provider error recovery delay elapsed.", display: false },
120
+ { triggerTurn: true },
121
+ );
122
+ }
123
+ : undefined,
124
+ });
125
+ return;
126
+ }
127
+
128
+ try {
129
+ consecutiveTransientErrors = 0;
130
+ networkRetryCounters.clear();
131
+ resolveAgentEnd(event);
132
+ } catch (err) {
133
+ const message = err instanceof Error ? err.message : String(err);
134
+ ctx.ui.notify(`Auto-mode error in agent_end handler: ${message}. Stopping auto-mode.`, "error");
135
+ try {
136
+ await pauseAuto(ctx, pi);
137
+ } catch {
138
+ // best-effort
139
+ }
140
+ }
141
+ }
142
+
@@ -0,0 +1,238 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
3
+
4
+ import { findMilestoneIds, nextMilestoneId } from "../guided-flow.js";
5
+ import { loadEffectiveGSDPreferences } from "../preferences.js";
6
+ import { ensureDbOpen } from "./dynamic-tools.js";
7
+
8
+ export function registerDbTools(pi: ExtensionAPI): void {
9
+ pi.registerTool({
10
+ name: "gsd_save_decision",
11
+ label: "Save Decision",
12
+ description:
13
+ "Record a project decision to the GSD database and regenerate DECISIONS.md. " +
14
+ "Decision IDs are auto-assigned — never provide an ID manually.",
15
+ promptSnippet: "Record a project decision to the GSD database (auto-assigns ID, regenerates DECISIONS.md)",
16
+ promptGuidelines: [
17
+ "Use gsd_save_decision when recording an architectural, pattern, library, or observability decision.",
18
+ "Decision IDs are auto-assigned (D001, D002, ...) — never guess or provide an ID.",
19
+ "All fields except revisable and when_context are required.",
20
+ "The tool writes to the DB and regenerates .gsd/DECISIONS.md automatically.",
21
+ ],
22
+ parameters: Type.Object({
23
+ scope: Type.String({ description: "Scope of the decision (e.g. 'architecture', 'library', 'observability')" }),
24
+ decision: Type.String({ description: "What is being decided" }),
25
+ choice: Type.String({ description: "The choice made" }),
26
+ rationale: Type.String({ description: "Why this choice was made" }),
27
+ revisable: Type.Optional(Type.String({ description: "Whether this can be revisited (default: 'Yes')" })),
28
+ when_context: Type.Optional(Type.String({ description: "When/context for the decision (e.g. milestone ID)" })),
29
+ }),
30
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
31
+ const dbAvailable = await ensureDbOpen();
32
+ if (!dbAvailable) {
33
+ return {
34
+ content: [{ type: "text" as const, text: "Error: GSD database is not available. Cannot save decision." }],
35
+ details: { operation: "save_decision", error: "db_unavailable" } as any,
36
+ };
37
+ }
38
+ try {
39
+ const { saveDecisionToDb } = await import("../db-writer.js");
40
+ const { id } = await saveDecisionToDb(
41
+ {
42
+ scope: params.scope,
43
+ decision: params.decision,
44
+ choice: params.choice,
45
+ rationale: params.rationale,
46
+ revisable: params.revisable,
47
+ when_context: params.when_context,
48
+ },
49
+ process.cwd(),
50
+ );
51
+ return {
52
+ content: [{ type: "text" as const, text: `Saved decision ${id}` }],
53
+ details: { operation: "save_decision", id } as any,
54
+ };
55
+ } catch (err) {
56
+ const msg = err instanceof Error ? err.message : String(err);
57
+ process.stderr.write(`gsd-db: gsd_save_decision tool failed: ${msg}\n`);
58
+ return {
59
+ content: [{ type: "text" as const, text: `Error saving decision: ${msg}` }],
60
+ details: { operation: "save_decision", error: msg } as any,
61
+ };
62
+ }
63
+ },
64
+ });
65
+
66
+ pi.registerTool({
67
+ name: "gsd_update_requirement",
68
+ label: "Update Requirement",
69
+ description:
70
+ "Update an existing requirement in the GSD database and regenerate REQUIREMENTS.md. " +
71
+ "Provide the requirement ID (e.g. R001) and any fields to update.",
72
+ promptSnippet: "Update an existing GSD requirement by ID (regenerates REQUIREMENTS.md)",
73
+ promptGuidelines: [
74
+ "Use gsd_update_requirement to change status, validation, notes, or other fields on an existing requirement.",
75
+ "The id parameter is required — it must be an existing RXXX identifier.",
76
+ "All other fields are optional — only provided fields are updated.",
77
+ "The tool verifies the requirement exists before updating.",
78
+ ],
79
+ parameters: Type.Object({
80
+ id: Type.String({ description: "The requirement ID (e.g. R001, R014)" }),
81
+ status: Type.Optional(Type.String({ description: "New status (e.g. 'active', 'validated', 'deferred')" })),
82
+ validation: Type.Optional(Type.String({ description: "Validation criteria or proof" })),
83
+ notes: Type.Optional(Type.String({ description: "Additional notes" })),
84
+ description: Type.Optional(Type.String({ description: "Updated description" })),
85
+ primary_owner: Type.Optional(Type.String({ description: "Primary owning slice" })),
86
+ supporting_slices: Type.Optional(Type.String({ description: "Supporting slices" })),
87
+ }),
88
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
89
+ const dbAvailable = await ensureDbOpen();
90
+ if (!dbAvailable) {
91
+ return {
92
+ content: [{ type: "text" as const, text: "Error: GSD database is not available. Cannot update requirement." }],
93
+ details: { operation: "update_requirement", id: params.id, error: "db_unavailable" } as any,
94
+ };
95
+ }
96
+ try {
97
+ const db = await import("../gsd-db.js");
98
+ const existing = db.getRequirementById(params.id);
99
+ if (!existing) {
100
+ return {
101
+ content: [{ type: "text" as const, text: `Error: Requirement ${params.id} not found.` }],
102
+ details: { operation: "update_requirement", id: params.id, error: "not_found" } as any,
103
+ };
104
+ }
105
+ const { updateRequirementInDb } = await import("../db-writer.js");
106
+ const updates: Record<string, string | undefined> = {};
107
+ if (params.status !== undefined) updates.status = params.status;
108
+ if (params.validation !== undefined) updates.validation = params.validation;
109
+ if (params.notes !== undefined) updates.notes = params.notes;
110
+ if (params.description !== undefined) updates.description = params.description;
111
+ if (params.primary_owner !== undefined) updates.primary_owner = params.primary_owner;
112
+ if (params.supporting_slices !== undefined) updates.supporting_slices = params.supporting_slices;
113
+ await updateRequirementInDb(params.id, updates, process.cwd());
114
+ return {
115
+ content: [{ type: "text" as const, text: `Updated requirement ${params.id}` }],
116
+ details: { operation: "update_requirement", id: params.id } as any,
117
+ };
118
+ } catch (err) {
119
+ const msg = err instanceof Error ? err.message : String(err);
120
+ process.stderr.write(`gsd-db: gsd_update_requirement tool failed: ${msg}\n`);
121
+ return {
122
+ content: [{ type: "text" as const, text: `Error updating requirement: ${msg}` }],
123
+ details: { operation: "update_requirement", id: params.id, error: msg } as any,
124
+ };
125
+ }
126
+ },
127
+ });
128
+
129
+ pi.registerTool({
130
+ name: "gsd_save_summary",
131
+ label: "Save Summary",
132
+ description:
133
+ "Save a summary, research, context, or assessment artifact to the GSD database and write it to disk. " +
134
+ "Computes the file path from milestone/slice/task IDs automatically.",
135
+ promptSnippet: "Save a GSD artifact (summary/research/context/assessment) to DB and disk",
136
+ promptGuidelines: [
137
+ "Use gsd_save_summary to persist structured artifacts (SUMMARY, RESEARCH, CONTEXT, ASSESSMENT).",
138
+ "milestone_id is required. slice_id and task_id are optional — they determine the file path.",
139
+ "The tool computes the relative path automatically: milestones/M001/M001-SUMMARY.md, milestones/M001/slices/S01/S01-SUMMARY.md, etc.",
140
+ "artifact_type must be one of: SUMMARY, RESEARCH, CONTEXT, ASSESSMENT.",
141
+ ],
142
+ parameters: Type.Object({
143
+ milestone_id: Type.String({ description: "Milestone ID (e.g. M001)" }),
144
+ slice_id: Type.Optional(Type.String({ description: "Slice ID (e.g. S01)" })),
145
+ task_id: Type.Optional(Type.String({ description: "Task ID (e.g. T01)" })),
146
+ artifact_type: Type.String({ description: "One of: SUMMARY, RESEARCH, CONTEXT, ASSESSMENT" }),
147
+ content: Type.String({ description: "The full markdown content of the artifact" }),
148
+ }),
149
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
150
+ const dbAvailable = await ensureDbOpen();
151
+ if (!dbAvailable) {
152
+ return {
153
+ content: [{ type: "text" as const, text: "Error: GSD database is not available. Cannot save artifact." }],
154
+ details: { operation: "save_summary", error: "db_unavailable" } as any,
155
+ };
156
+ }
157
+ const validTypes = ["SUMMARY", "RESEARCH", "CONTEXT", "ASSESSMENT"];
158
+ if (!validTypes.includes(params.artifact_type)) {
159
+ return {
160
+ content: [{ type: "text" as const, text: `Error: Invalid artifact_type "${params.artifact_type}". Must be one of: ${validTypes.join(", ")}` }],
161
+ details: { operation: "save_summary", error: "invalid_artifact_type" } as any,
162
+ };
163
+ }
164
+ try {
165
+ let relativePath: string;
166
+ if (params.task_id && params.slice_id) {
167
+ relativePath = `milestones/${params.milestone_id}/slices/${params.slice_id}/tasks/${params.task_id}-${params.artifact_type}.md`;
168
+ } else if (params.slice_id) {
169
+ relativePath = `milestones/${params.milestone_id}/slices/${params.slice_id}/${params.slice_id}-${params.artifact_type}.md`;
170
+ } else {
171
+ relativePath = `milestones/${params.milestone_id}/${params.milestone_id}-${params.artifact_type}.md`;
172
+ }
173
+ const { saveArtifactToDb } = await import("../db-writer.js");
174
+ await saveArtifactToDb(
175
+ {
176
+ path: relativePath,
177
+ artifact_type: params.artifact_type,
178
+ content: params.content,
179
+ milestone_id: params.milestone_id,
180
+ slice_id: params.slice_id,
181
+ task_id: params.task_id,
182
+ },
183
+ process.cwd(),
184
+ );
185
+ return {
186
+ content: [{ type: "text" as const, text: `Saved ${params.artifact_type} artifact to ${relativePath}` }],
187
+ details: { operation: "save_summary", path: relativePath, artifact_type: params.artifact_type } as any,
188
+ };
189
+ } catch (err) {
190
+ const msg = err instanceof Error ? err.message : String(err);
191
+ process.stderr.write(`gsd-db: gsd_save_summary tool failed: ${msg}\n`);
192
+ return {
193
+ content: [{ type: "text" as const, text: `Error saving artifact: ${msg}` }],
194
+ details: { operation: "save_summary", error: msg } as any,
195
+ };
196
+ }
197
+ },
198
+ });
199
+
200
+ const reservedMilestoneIds = new Set<string>();
201
+ pi.registerTool({
202
+ name: "gsd_generate_milestone_id",
203
+ label: "Generate Milestone ID",
204
+ description:
205
+ "Generate the next milestone ID for a new GSD milestone. " +
206
+ "Scans existing milestones on disk and respects the unique_milestone_ids preference. " +
207
+ "Always use this tool when creating a new milestone — never invent milestone IDs manually.",
208
+ promptSnippet: "Generate a valid milestone ID (respects unique_milestone_ids preference)",
209
+ promptGuidelines: [
210
+ "ALWAYS call gsd_generate_milestone_id before creating a new milestone directory or writing milestone files.",
211
+ "Never invent or hardcode milestone IDs like M001, M002 — always use this tool.",
212
+ "Call it once per milestone you need to create. For multi-milestone projects, call it once for each milestone in sequence.",
213
+ "The tool returns the correct format based on project preferences (e.g. M001 or M001-r5jzab).",
214
+ ],
215
+ parameters: Type.Object({}),
216
+ async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
217
+ try {
218
+ const basePath = process.cwd();
219
+ const existingIds = findMilestoneIds(basePath);
220
+ const uniqueEnabled = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
221
+ const allIds = [...new Set([...existingIds, ...reservedMilestoneIds])];
222
+ const newId = nextMilestoneId(allIds, uniqueEnabled);
223
+ reservedMilestoneIds.add(newId);
224
+ return {
225
+ content: [{ type: "text" as const, text: newId }],
226
+ details: { operation: "generate_milestone_id", id: newId, existingCount: existingIds.length, reservedCount: reservedMilestoneIds.size, uniqueEnabled } as any,
227
+ };
228
+ } catch (err) {
229
+ const msg = err instanceof Error ? err.message : String(err);
230
+ return {
231
+ content: [{ type: "text" as const, text: `Error generating milestone ID: ${msg}` }],
232
+ details: { operation: "generate_milestone_id", error: msg } as any,
233
+ };
234
+ }
235
+ },
236
+ });
237
+ }
238
+
@@ -0,0 +1,90 @@
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ import type { ExtensionAPI } from "@gsd/pi-coding-agent";
5
+ import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@gsd/pi-coding-agent";
6
+
7
+ import { DEFAULT_BASH_TIMEOUT_SECS } from "../constants.js";
8
+
9
+ export async function ensureDbOpen(): Promise<boolean> {
10
+ try {
11
+ const db = await import("../gsd-db.js");
12
+ if (db.isDbAvailable()) return true;
13
+ const dbPath = join(process.cwd(), ".gsd", "gsd.db");
14
+ if (existsSync(dbPath)) {
15
+ return db.openDatabase(dbPath);
16
+ }
17
+ return false;
18
+ } catch {
19
+ return false;
20
+ }
21
+ }
22
+
23
+ export function registerDynamicTools(pi: ExtensionAPI): void {
24
+ const baseBash = createBashTool(process.cwd(), {
25
+ spawnHook: (ctx) => ({ ...ctx, cwd: process.cwd() }),
26
+ });
27
+ const dynamicBash = {
28
+ ...baseBash,
29
+ execute: async (
30
+ toolCallId: string,
31
+ params: { command: string; timeout?: number },
32
+ signal?: AbortSignal,
33
+ onUpdate?: unknown,
34
+ ctx?: unknown,
35
+ ) => {
36
+ const paramsWithTimeout = {
37
+ ...params,
38
+ timeout: params.timeout ?? DEFAULT_BASH_TIMEOUT_SECS,
39
+ };
40
+ return (baseBash as any).execute(toolCallId, paramsWithTimeout, signal, onUpdate, ctx);
41
+ },
42
+ };
43
+ pi.registerTool(dynamicBash as any);
44
+
45
+ const baseWrite = createWriteTool(process.cwd());
46
+ pi.registerTool({
47
+ ...baseWrite,
48
+ execute: async (
49
+ toolCallId: string,
50
+ params: { path: string; content: string },
51
+ signal?: AbortSignal,
52
+ onUpdate?: unknown,
53
+ ctx?: unknown,
54
+ ) => {
55
+ const fresh = createWriteTool(process.cwd());
56
+ return (fresh as any).execute(toolCallId, params, signal, onUpdate, ctx);
57
+ },
58
+ } as any);
59
+
60
+ const baseRead = createReadTool(process.cwd());
61
+ pi.registerTool({
62
+ ...baseRead,
63
+ execute: async (
64
+ toolCallId: string,
65
+ params: { path: string; offset?: number; limit?: number },
66
+ signal?: AbortSignal,
67
+ onUpdate?: unknown,
68
+ ctx?: unknown,
69
+ ) => {
70
+ const fresh = createReadTool(process.cwd());
71
+ return (fresh as any).execute(toolCallId, params, signal, onUpdate, ctx);
72
+ },
73
+ } as any);
74
+
75
+ const baseEdit = createEditTool(process.cwd());
76
+ pi.registerTool({
77
+ ...baseEdit,
78
+ execute: async (
79
+ toolCallId: string,
80
+ params: { path: string; oldText: string; newText: string },
81
+ signal?: AbortSignal,
82
+ onUpdate?: unknown,
83
+ ctx?: unknown,
84
+ ) => {
85
+ const fresh = createEditTool(process.cwd());
86
+ return (fresh as any).execute(toolCallId, params, signal, onUpdate, ctx);
87
+ },
88
+ } as any);
89
+ }
90
+
@@ -0,0 +1,46 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
2
+
3
+ import { registerGSDCommand } from "../commands.js";
4
+ import { registerExitCommand } from "../exit-command.js";
5
+ import { registerWorktreeCommand } from "../worktree-command.js";
6
+ import { registerDbTools } from "./db-tools.js";
7
+ import { registerDynamicTools } from "./dynamic-tools.js";
8
+ import { registerHooks } from "./register-hooks.js";
9
+ import { registerShortcuts } from "./register-shortcuts.js";
10
+
11
+ function installEpipeGuard(): void {
12
+ if (!process.listeners("uncaughtException").some((listener) => listener.name === "_gsdEpipeGuard")) {
13
+ const _gsdEpipeGuard = (err: Error): void => {
14
+ if ((err as NodeJS.ErrnoException).code === "EPIPE") {
15
+ process.exit(0);
16
+ }
17
+ if ((err as NodeJS.ErrnoException).code === "ENOENT" && (err as any).syscall?.startsWith("spawn")) {
18
+ process.stderr.write(`[gsd] spawn ENOENT: ${(err as any).path ?? "unknown"} — command not found\n`);
19
+ return;
20
+ }
21
+ throw err;
22
+ };
23
+ process.on("uncaughtException", _gsdEpipeGuard);
24
+ }
25
+ }
26
+
27
+ export function registerGsdExtension(pi: ExtensionAPI): void {
28
+ registerGSDCommand(pi);
29
+ registerWorktreeCommand(pi);
30
+ registerExitCommand(pi);
31
+
32
+ installEpipeGuard();
33
+
34
+ pi.registerCommand("kill", {
35
+ description: "Exit GSD immediately (no cleanup)",
36
+ handler: async (_args: string, _ctx: ExtensionCommandContext) => {
37
+ process.exit(0);
38
+ },
39
+ });
40
+
41
+ registerDynamicTools(pi);
42
+ registerDbTools(pi);
43
+ registerShortcuts(pi);
44
+ registerHooks(pi);
45
+ }
46
+