gsd-pi 2.59.0-dev.d77b3dd → 2.60.0-dev.2580e65

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 (198) hide show
  1. package/dist/resources/extensions/ask-user-questions.js +7 -4
  2. package/dist/resources/extensions/gsd/auto/phases.js +15 -7
  3. package/dist/resources/extensions/gsd/auto-dashboard.js +21 -8
  4. package/dist/resources/extensions/gsd/auto-dispatch.js +6 -3
  5. package/dist/resources/extensions/gsd/auto-model-selection.js +58 -9
  6. package/dist/resources/extensions/gsd/auto-post-unit.js +3 -2
  7. package/dist/resources/extensions/gsd/auto-prompts.js +36 -20
  8. package/dist/resources/extensions/gsd/auto-recovery.js +37 -18
  9. package/dist/resources/extensions/gsd/auto-start.js +9 -5
  10. package/dist/resources/extensions/gsd/auto-timers.js +11 -5
  11. package/dist/resources/extensions/gsd/auto-unit-closeout.js +5 -3
  12. package/dist/resources/extensions/gsd/auto-verification.js +3 -2
  13. package/dist/resources/extensions/gsd/auto-worktree.js +120 -55
  14. package/dist/resources/extensions/gsd/auto.js +39 -17
  15. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +6 -3
  16. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +2 -2
  17. package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +4 -10
  18. package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +2 -1
  19. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +7 -0
  20. package/dist/resources/extensions/gsd/bootstrap/system-context.js +11 -10
  21. package/dist/resources/extensions/gsd/commands/catalog.js +2 -0
  22. package/dist/resources/extensions/gsd/commands-codebase.js +48 -21
  23. package/dist/resources/extensions/gsd/commands-inspect.js +2 -1
  24. package/dist/resources/extensions/gsd/commands-maintenance.js +32 -19
  25. package/dist/resources/extensions/gsd/complexity-classifier.js +8 -4
  26. package/dist/resources/extensions/gsd/custom-verification.js +3 -2
  27. package/dist/resources/extensions/gsd/gsd-db.js +33 -13
  28. package/dist/resources/extensions/gsd/guided-flow.js +19 -9
  29. package/dist/resources/extensions/gsd/init-wizard.js +12 -0
  30. package/dist/resources/extensions/gsd/markdown-renderer.js +11 -9
  31. package/dist/resources/extensions/gsd/md-importer.js +5 -4
  32. package/dist/resources/extensions/gsd/milestone-actions.js +3 -2
  33. package/dist/resources/extensions/gsd/milestone-ids.js +2 -1
  34. package/dist/resources/extensions/gsd/model-router.js +156 -121
  35. package/dist/resources/extensions/gsd/parallel-merge.js +5 -3
  36. package/dist/resources/extensions/gsd/parallel-orchestrator.js +26 -14
  37. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  38. package/dist/resources/extensions/gsd/preferences-validation.js +45 -0
  39. package/dist/resources/extensions/gsd/preferences.js +15 -3
  40. package/dist/resources/extensions/gsd/prompt-loader.js +3 -2
  41. package/dist/resources/extensions/gsd/prompts/rethink.md +1 -1
  42. package/dist/resources/extensions/gsd/rule-registry.js +7 -6
  43. package/dist/resources/extensions/gsd/safe-fs.js +6 -8
  44. package/dist/resources/extensions/gsd/tools/complete-milestone.js +3 -2
  45. package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -2
  46. package/dist/resources/extensions/gsd/tools/complete-task.js +3 -2
  47. package/dist/resources/extensions/gsd/tools/plan-milestone.js +3 -2
  48. package/dist/resources/extensions/gsd/tools/plan-slice.js +3 -2
  49. package/dist/resources/extensions/gsd/tools/plan-task.js +2 -1
  50. package/dist/resources/extensions/gsd/tools/reassess-roadmap.js +4 -4
  51. package/dist/resources/extensions/gsd/tools/reopen-slice.js +2 -1
  52. package/dist/resources/extensions/gsd/tools/reopen-task.js +2 -1
  53. package/dist/resources/extensions/gsd/tools/replan-slice.js +2 -1
  54. package/dist/resources/extensions/gsd/tools/validate-milestone.js +2 -1
  55. package/dist/resources/extensions/gsd/triage-resolution.js +11 -4
  56. package/dist/resources/extensions/gsd/workflow-events.js +2 -1
  57. package/dist/resources/extensions/gsd/workflow-logger.js +37 -4
  58. package/dist/resources/extensions/gsd/workflow-migration.js +14 -12
  59. package/dist/resources/extensions/gsd/workflow-projections.js +2 -2
  60. package/dist/resources/extensions/gsd/workflow-reconcile.js +2 -2
  61. package/dist/resources/extensions/gsd/worktree-manager.js +26 -14
  62. package/dist/resources/extensions/shared/interview-ui.js +3 -1
  63. package/dist/web/standalone/.next/BUILD_ID +1 -1
  64. package/dist/web/standalone/.next/app-path-routes-manifest.json +19 -19
  65. package/dist/web/standalone/.next/build-manifest.json +2 -2
  66. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  67. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  68. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  69. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  70. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  71. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  72. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  73. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  74. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  75. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  76. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  77. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  78. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  79. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  80. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  81. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  82. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  83. package/dist/web/standalone/.next/server/app/index.html +1 -1
  84. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  85. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  86. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  87. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  88. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  89. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  90. package/dist/web/standalone/.next/server/app-paths-manifest.json +19 -19
  91. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  92. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  93. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  94. package/package.json +1 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/extensions/loader.js +5 -0
  97. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -1
  99. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/core/extensions/runner.js +16 -0
  101. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +26 -0
  103. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -1
  106. package/packages/pi-coding-agent/dist/core/lsp/config.js +6 -1
  107. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -1
  108. package/packages/pi-coding-agent/dist/core/lsp/defaults.json +2 -2
  109. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.d.ts +2 -0
  110. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.d.ts.map +1 -0
  111. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.js +47 -0
  112. package/packages/pi-coding-agent/dist/core/lsp/lsp-legacy-alias.test.js.map +1 -0
  113. package/packages/pi-coding-agent/package.json +1 -1
  114. package/packages/pi-coding-agent/src/core/extensions/loader.ts +6 -0
  115. package/packages/pi-coding-agent/src/core/extensions/runner.ts +19 -0
  116. package/packages/pi-coding-agent/src/core/extensions/types.ts +26 -0
  117. package/packages/pi-coding-agent/src/core/lsp/config.ts +7 -1
  118. package/packages/pi-coding-agent/src/core/lsp/defaults.json +2 -2
  119. package/packages/pi-coding-agent/src/core/lsp/lsp-legacy-alias.test.ts +70 -0
  120. package/pkg/package.json +1 -1
  121. package/src/resources/extensions/ask-user-questions.ts +7 -3
  122. package/src/resources/extensions/gsd/auto/phases.ts +17 -7
  123. package/src/resources/extensions/gsd/auto-dashboard.ts +22 -8
  124. package/src/resources/extensions/gsd/auto-dispatch.ts +7 -3
  125. package/src/resources/extensions/gsd/auto-model-selection.ts +77 -15
  126. package/src/resources/extensions/gsd/auto-post-unit.ts +4 -4
  127. package/src/resources/extensions/gsd/auto-prompts.ts +37 -20
  128. package/src/resources/extensions/gsd/auto-recovery.ts +38 -18
  129. package/src/resources/extensions/gsd/auto-start.ts +10 -9
  130. package/src/resources/extensions/gsd/auto-timers.ts +12 -5
  131. package/src/resources/extensions/gsd/auto-unit-closeout.ts +6 -2
  132. package/src/resources/extensions/gsd/auto-verification.ts +3 -6
  133. package/src/resources/extensions/gsd/auto-worktree.ts +121 -55
  134. package/src/resources/extensions/gsd/auto.ts +40 -17
  135. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +4 -3
  136. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +2 -2
  137. package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +4 -16
  138. package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +2 -1
  139. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +8 -0
  140. package/src/resources/extensions/gsd/bootstrap/system-context.ts +11 -10
  141. package/src/resources/extensions/gsd/commands/catalog.ts +2 -0
  142. package/src/resources/extensions/gsd/commands-codebase.ts +52 -20
  143. package/src/resources/extensions/gsd/commands-inspect.ts +2 -1
  144. package/src/resources/extensions/gsd/commands-maintenance.ts +28 -19
  145. package/src/resources/extensions/gsd/complexity-classifier.ts +9 -4
  146. package/src/resources/extensions/gsd/custom-verification.ts +3 -2
  147. package/src/resources/extensions/gsd/gsd-db.ts +12 -14
  148. package/src/resources/extensions/gsd/guided-flow.ts +9 -8
  149. package/src/resources/extensions/gsd/init-wizard.ts +12 -0
  150. package/src/resources/extensions/gsd/markdown-renderer.ts +11 -17
  151. package/src/resources/extensions/gsd/md-importer.ts +5 -4
  152. package/src/resources/extensions/gsd/milestone-actions.ts +3 -2
  153. package/src/resources/extensions/gsd/milestone-ids.ts +2 -1
  154. package/src/resources/extensions/gsd/model-router.ts +199 -173
  155. package/src/resources/extensions/gsd/parallel-merge.ts +5 -3
  156. package/src/resources/extensions/gsd/parallel-orchestrator.ts +18 -14
  157. package/src/resources/extensions/gsd/preferences-types.ts +13 -0
  158. package/src/resources/extensions/gsd/preferences-validation.ts +45 -0
  159. package/src/resources/extensions/gsd/preferences.ts +16 -3
  160. package/src/resources/extensions/gsd/prompt-loader.ts +3 -2
  161. package/src/resources/extensions/gsd/prompts/rethink.md +1 -1
  162. package/src/resources/extensions/gsd/rule-registry.ts +7 -6
  163. package/src/resources/extensions/gsd/safe-fs.ts +6 -5
  164. package/src/resources/extensions/gsd/tests/capability-router.test.ts +347 -0
  165. package/src/resources/extensions/gsd/tests/codebase-generator.test.ts +63 -0
  166. package/src/resources/extensions/gsd/tests/complexity-classifier.test.ts +27 -2
  167. package/src/resources/extensions/gsd/tests/db-path-worktree-symlink.test.ts +4 -4
  168. package/src/resources/extensions/gsd/tests/integration/state-machine-edge-cases.test.ts +1188 -0
  169. package/src/resources/extensions/gsd/tests/integration/state-machine-runtime-failures.test.ts +841 -0
  170. package/src/resources/extensions/gsd/tests/model-router.test.ts +403 -3
  171. package/src/resources/extensions/gsd/tests/preferences.test.ts +62 -0
  172. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +21 -0
  173. package/src/resources/extensions/gsd/tests/silent-catch-diagnostics.test.ts +284 -0
  174. package/src/resources/extensions/gsd/tests/workflow-logger-audit.test.ts +120 -0
  175. package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +6 -6
  176. package/src/resources/extensions/gsd/tools/complete-milestone.ts +3 -6
  177. package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -6
  178. package/src/resources/extensions/gsd/tools/complete-task.ts +3 -6
  179. package/src/resources/extensions/gsd/tools/plan-milestone.ts +3 -6
  180. package/src/resources/extensions/gsd/tools/plan-slice.ts +3 -6
  181. package/src/resources/extensions/gsd/tools/plan-task.ts +2 -3
  182. package/src/resources/extensions/gsd/tools/reassess-roadmap.ts +4 -6
  183. package/src/resources/extensions/gsd/tools/reopen-slice.ts +2 -3
  184. package/src/resources/extensions/gsd/tools/reopen-task.ts +2 -3
  185. package/src/resources/extensions/gsd/tools/replan-slice.ts +2 -3
  186. package/src/resources/extensions/gsd/tools/validate-milestone.ts +2 -3
  187. package/src/resources/extensions/gsd/triage-resolution.ts +11 -4
  188. package/src/resources/extensions/gsd/types.ts +1 -0
  189. package/src/resources/extensions/gsd/workflow-events.ts +2 -1
  190. package/src/resources/extensions/gsd/workflow-logger.ts +52 -5
  191. package/src/resources/extensions/gsd/workflow-migration.ts +14 -12
  192. package/src/resources/extensions/gsd/workflow-projections.ts +2 -2
  193. package/src/resources/extensions/gsd/workflow-reconcile.ts +2 -2
  194. package/src/resources/extensions/gsd/worktree-manager.ts +16 -14
  195. package/src/resources/extensions/shared/interview-ui.ts +3 -1
  196. package/src/resources/extensions/shared/tests/interview-notes-loop.test.ts +144 -0
  197. /package/dist/web/standalone/.next/static/{t_cBZAENjaOJIRST3dw08 → ogyMN7M-3bGGuRY08L5HR}/_buildManifest.js +0 -0
  198. /package/dist/web/standalone/.next/static/{t_cBZAENjaOJIRST3dw08 → ogyMN7M-3bGGuRY08L5HR}/_ssgManifest.js +0 -0
@@ -603,6 +603,22 @@ export interface ModelSelectEvent {
603
603
  source: ModelSelectSource;
604
604
  }
605
605
 
606
+ /** Fired before model selection runs capability scoring. Extensions can override the selected model. */
607
+ export interface BeforeModelSelectEvent {
608
+ type: "before_model_select";
609
+ unitType: string;
610
+ unitId: string;
611
+ classification: { tier: string; reason: string; downgraded: boolean };
612
+ taskMetadata?: Record<string, unknown>;
613
+ eligibleModels: string[];
614
+ phaseConfig?: { primary: string; fallbacks: string[] };
615
+ }
616
+
617
+ /** Result from before_model_select event handler. Return { modelId } to override selection. */
618
+ export interface BeforeModelSelectResult {
619
+ modelId: string;
620
+ }
621
+
606
622
  // ============================================================================
607
623
  // User Bash Events
608
624
  // ============================================================================
@@ -1052,6 +1068,14 @@ export interface ExtensionAPI {
1052
1068
  on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
1053
1069
  on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
1054
1070
  on(event: "input", handler: ExtensionHandler<InputEvent, InputEventResult>): void;
1071
+ on(event: "before_model_select", handler: ExtensionHandler<BeforeModelSelectEvent, BeforeModelSelectResult>): void;
1072
+
1073
+ // =========================================================================
1074
+ // Event Emission (for host extensions that orchestrate model selection)
1075
+ // =========================================================================
1076
+
1077
+ /** Emit before_model_select event. Returns override model ID or undefined. */
1078
+ emitBeforeModelSelect(event: Omit<BeforeModelSelectEvent, "type">): Promise<BeforeModelSelectResult | undefined>;
1055
1079
 
1056
1080
  // =========================================================================
1057
1081
  // Tool Registration
@@ -1367,6 +1391,8 @@ export interface ExtensionRuntimeState {
1367
1391
  */
1368
1392
  registerProvider: (name: string, config: ProviderConfig) => void;
1369
1393
  unregisterProvider: (name: string) => void;
1394
+ /** Emit before_model_select event to all registered handlers. Bound by ExtensionRunner. */
1395
+ emitBeforeModelSelect: (event: Omit<BeforeModelSelectEvent, "type">) => Promise<BeforeModelSelectResult | undefined>;
1370
1396
  }
1371
1397
 
1372
1398
  /**
@@ -12,6 +12,11 @@ import type { ServerConfig } from "./types.js";
12
12
  const require = createRequire(import.meta.url);
13
13
  const DEFAULTS = require("./defaults.json") as Record<string, Partial<ServerConfig>>;
14
14
 
15
+ /** Map legacy server keys to their current names so user overrides still merge. */
16
+ const LEGACY_ALIASES: Record<string, string> = {
17
+ "kotlin-language-server": "kotlin-lsp",
18
+ };
19
+
15
20
  export interface LspConfig {
16
21
  servers: Record<string, ServerConfig>;
17
22
  /** Idle timeout in milliseconds. If set, LSP clients will be shutdown after this period of inactivity. Disabled by default. */
@@ -109,7 +114,8 @@ function mergeServers(
109
114
  overrides: Record<string, Partial<ServerConfig>>,
110
115
  ): Record<string, ServerConfig> {
111
116
  const merged: Record<string, ServerConfig> = { ...base };
112
- for (const [name, config] of Object.entries(overrides)) {
117
+ for (const [rawName, config] of Object.entries(overrides)) {
118
+ const name = LEGACY_ALIASES[rawName] ?? rawName;
113
119
  if (merged[name]) {
114
120
  const candidate = { ...merged[name], ...config };
115
121
  const normalized = normalizeServerConfig(name, candidate);
@@ -189,8 +189,8 @@
189
189
  "fileTypes": [".java"],
190
190
  "rootMarkers": ["pom.xml", "build.gradle", "build.gradle.kts", "settings.gradle", ".project"]
191
191
  },
192
- "kotlin-language-server": {
193
- "command": "kotlin-language-server",
192
+ "kotlin-lsp": {
193
+ "command": "kotlin-lsp",
194
194
  "args": [],
195
195
  "fileTypes": [".kt", ".kts"],
196
196
  "rootMarkers": ["build.gradle", "build.gradle.kts", "pom.xml", "settings.gradle", "settings.gradle.kts"]
@@ -0,0 +1,70 @@
1
+ // GSD2 — Regression test for LSP legacy server key aliases
2
+ // Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
3
+
4
+ /**
5
+ * When a default server key is renamed (e.g., kotlin-language-server → kotlin-lsp),
6
+ * user overrides referencing the old key must still merge correctly via LEGACY_ALIASES.
7
+ *
8
+ * This test exercises the merge path through loadConfig() with a temp project
9
+ * containing an lsp.json that uses the legacy key.
10
+ */
11
+
12
+ import { describe, it, beforeEach, afterEach } from "node:test";
13
+ import assert from "node:assert/strict";
14
+ import * as fs from "node:fs";
15
+ import * as path from "node:path";
16
+ import * as os from "node:os";
17
+ import { loadConfig } from "./config.js";
18
+
19
+ describe("LSP legacy server key aliases", () => {
20
+ let tmpDir: string;
21
+
22
+ beforeEach(() => {
23
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "lsp-alias-test-"));
24
+ });
25
+
26
+ afterEach(() => {
27
+ fs.rmSync(tmpDir, { recursive: true, force: true });
28
+ });
29
+
30
+ it("merges user override with legacy key 'kotlin-language-server' into 'kotlin-lsp'", () => {
31
+ // Write an lsp.json that uses the old key name with a command that exists (node)
32
+ // so resolveCommand doesn't filter it out.
33
+ const overrideConfig = {
34
+ servers: {
35
+ "kotlin-language-server": {
36
+ command: "node",
37
+ },
38
+ },
39
+ };
40
+ fs.writeFileSync(
41
+ path.join(tmpDir, "lsp.json"),
42
+ JSON.stringify(overrideConfig),
43
+ );
44
+
45
+ // Also add root markers so the server is detected
46
+ fs.writeFileSync(path.join(tmpDir, "build.gradle.kts"), "");
47
+
48
+ const config = loadConfig(tmpDir);
49
+
50
+ // The merged config should have kotlin-lsp (new key) with the user's command override
51
+ const kotlinServer = config.servers["kotlin-lsp"];
52
+ assert.ok(kotlinServer, "kotlin-lsp should exist in merged config");
53
+ assert.equal(
54
+ kotlinServer.command,
55
+ "node",
56
+ "command should be overridden from user config via legacy alias",
57
+ );
58
+ assert.ok(
59
+ kotlinServer.fileTypes.includes(".kt"),
60
+ "fileTypes should be inherited from defaults",
61
+ );
62
+
63
+ // The old key should NOT appear as a separate entry
64
+ assert.equal(
65
+ config.servers["kotlin-language-server"],
66
+ undefined,
67
+ "legacy key should not appear as separate server",
68
+ );
69
+ });
70
+ });
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittercowboy/gsd",
3
- "version": "2.59.0",
3
+ "version": "2.60.0",
4
4
  "piConfig": {
5
5
  "name": "gsd",
6
6
  "configDir": ".gsd"
@@ -135,10 +135,14 @@ export default function AskUserQuestions(pi: ExtensionAPI) {
135
135
  }
136
136
  }
137
137
 
138
+ // Try remote first if configured (works in both interactive and headless modes).
139
+ // tryRemoteQuestions returns null when no remote channel is configured, so
140
+ // this is a no-op when the user has not set up Slack/Discord/Telegram.
141
+ const { tryRemoteQuestions } = await import("./remote-questions/manager.js");
142
+ const remoteResult = await tryRemoteQuestions(params.questions, signal);
143
+ if (remoteResult) return { ...remoteResult, details: remoteResult.details as unknown };
144
+
138
145
  if (!ctx.hasUI) {
139
- const { tryRemoteQuestions } = await import("./remote-questions/manager.js");
140
- const remoteResult = await tryRemoteQuestions(params.questions, signal);
141
- if (remoteResult) return { ...remoteResult, details: remoteResult.details as unknown };
142
146
  return errorResult("Error: UI not available (non-interactive mode)", params.questions);
143
147
  }
144
148
 
@@ -721,6 +721,8 @@ export async function runGuards(
721
721
  // ── Stop/Backtrack directive guard (#3487) ──
722
722
  // Check for unexecuted stop or backtrack captures BEFORE dispatching any unit.
723
723
  // This ensures user "halt" directives are honored immediately.
724
+ // IMPORTANT: Fail-closed — any exception during stop handling still breaks the loop
725
+ // to ensure user halt intent is never silently dropped.
724
726
  try {
725
727
  const { loadStopCaptures, markCaptureExecuted } = await import("../captures.js");
726
728
  const stopCaptures = loadStopCaptures(s.basePath);
@@ -737,12 +739,10 @@ export async function runGuards(
737
739
  basename(s.originalBasePath || s.basePath),
738
740
  );
739
741
 
740
- // Mark all stop/backtrack captures as executed so they don't re-fire
741
- for (const cap of stopCaptures) {
742
- markCaptureExecuted(s.basePath, cap.id);
743
- }
742
+ // Pause first ensures auto-mode stops even if later steps fail
743
+ await deps.pauseAuto(ctx, pi);
744
744
 
745
- // For backtrack captures, write the backtrack trigger before pausing
745
+ // For backtrack captures, write the backtrack trigger after pausing
746
746
  if (isBacktrack) {
747
747
  try {
748
748
  const { executeBacktrack } = await import("../triage-resolution.js");
@@ -752,12 +752,19 @@ export async function runGuards(
752
752
  }
753
753
  }
754
754
 
755
- await deps.pauseAuto(ctx, pi);
755
+ // Mark captures as executed only after successful pause/transition
756
+ for (const cap of stopCaptures) {
757
+ markCaptureExecuted(s.basePath, cap.id);
758
+ }
759
+
756
760
  debugLog("autoLoop", { phase: "exit", reason: isBacktrack ? "user-backtrack" : "user-stop" });
757
761
  return { action: "break", reason: isBacktrack ? "user-backtrack" : "user-stop" };
758
762
  }
759
763
  } catch (e) {
764
+ // Fail-closed: if anything in the stop guard throws, break the loop
765
+ // rather than silently continuing and dropping user halt intent
760
766
  debugLog("guards", { phase: "stop-guard-error", error: String(e) });
767
+ return { action: "break", reason: "stop-guard-error" };
761
768
  }
762
769
 
763
770
  // Budget ceiling guard
@@ -1261,7 +1268,9 @@ export async function runUnitPhase(
1261
1268
  blockers: [],
1262
1269
  nextSteps: [],
1263
1270
  });
1264
- } catch { /* non-fatal — anchor is advisory */ }
1271
+ } catch (err) { /* non-fatal — anchor is advisory */
1272
+ logWarning("engine", `phase anchor failed: ${err instanceof Error ? err.message : String(err)}`);
1273
+ }
1265
1274
  }
1266
1275
 
1267
1276
  deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "unit-end", data: { unitType, unitId, status: unitResult.status, artifactVerified, ...(unitResult.errorContext ? { errorContext: unitResult.errorContext } : {}) }, causedBy: { flowId: ic.flowId, seq: unitStartSeq } });
@@ -1384,3 +1393,4 @@ export async function runFinalize(
1384
1393
 
1385
1394
  return { action: "next", data: undefined as void };
1386
1395
  }
1396
+
@@ -31,6 +31,7 @@ import {
31
31
  getRtkSessionSavings,
32
32
  type RtkSessionSavings,
33
33
  } from "../shared/rtk-session-stats.js";
34
+ import { logWarning } from "./workflow-logger.js";
34
35
 
35
36
  // ─── UAT Slice Extraction ─────────────────────────────────────────────────────
36
37
 
@@ -285,8 +286,9 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
285
286
  taskDetails = dbTasks.map(t => ({ id: t.id, title: t.title, done: t.status === "complete" || t.status === "done" }));
286
287
  }
287
288
  }
288
- } catch {
289
+ } catch (err) {
289
290
  // Non-fatal — just omit task count
291
+ logWarning("dashboard", `operation failed: ${err instanceof Error ? err.message : String(err)}`);
290
292
  }
291
293
  }
292
294
 
@@ -297,8 +299,9 @@ export function updateSliceProgressCache(base: string, mid: string, activeSid?:
297
299
  activeSliceTasks,
298
300
  taskDetails,
299
301
  };
300
- } catch {
302
+ } catch (err) {
301
303
  // Non-fatal — widget just won't show progress bar
304
+ logWarning("dashboard", `operation failed: ${err instanceof Error ? err.message : String(err)}`);
302
305
  }
303
306
  }
304
307
 
@@ -332,8 +335,9 @@ function refreshLastCommit(basePath: string): void {
332
335
  };
333
336
  }
334
337
  lastCommitFetchedAt = Date.now();
335
- } catch {
338
+ } catch (err) {
336
339
  // Non-fatal — just skip last commit display
340
+ logWarning("dashboard", `operation failed: ${err instanceof Error ? err.message : String(err)}`);
337
341
  }
338
342
  }
339
343
 
@@ -376,7 +380,9 @@ function ensureWidgetModeLoaded(): void {
376
380
  if (saved && WIDGET_MODES.includes(saved as WidgetMode)) {
377
381
  widgetMode = saved as WidgetMode;
378
382
  }
379
- } catch { /* non-fatal — use default */ }
383
+ } catch (err) { /* non-fatal — use default */
384
+ logWarning("dashboard", `operation failed: ${err instanceof Error ? err.message : String(err)}`);
385
+ }
380
386
  }
381
387
 
382
388
  /** Persist widget mode to global preferences YAML. */
@@ -395,7 +401,9 @@ function persistWidgetMode(mode: WidgetMode): void {
395
401
  content = content.trimEnd() + "\n" + line + "\n";
396
402
  }
397
403
  writeFileSync(prefsPath, content, "utf-8");
398
- } catch { /* non-fatal — mode still set in memory */ }
404
+ } catch (err) { /* non-fatal — mode still set in memory */
405
+ logWarning("dashboard", `file write failed: ${err instanceof Error ? err.message : String(err)}`);
406
+ }
399
407
  }
400
408
 
401
409
  /** Cycle to the next widget mode. Returns the new mode. */
@@ -458,7 +466,9 @@ export function updateProgressWidget(
458
466
 
459
467
  // Cache git branch at widget creation time (not per render)
460
468
  let cachedBranch: string | null = null;
461
- try { cachedBranch = getCurrentBranch(accessors.getBasePath()); } catch { /* not in git repo */ }
469
+ try { cachedBranch = getCurrentBranch(accessors.getBasePath()); } catch (err) { /* not in git repo */
470
+ logWarning("dashboard", `git branch detection failed: ${err instanceof Error ? err.message : String(err)}`);
471
+ }
462
472
 
463
473
  // Cache short pwd (last 2 path segments only) + worktree/branch info
464
474
  let widgetPwd: string;
@@ -495,7 +505,8 @@ export function updateProgressWidget(
495
505
  const sessionId = ctx.sessionManager.getSessionId();
496
506
  const savings = sessionId ? getRtkSessionSavings(accessors.getBasePath(), sessionId) : null;
497
507
  cachedRtkLabel = formatRtkSavingsLabel(savings);
498
- } catch {
508
+ } catch (err) {
509
+ logWarning("dashboard", `RTK savings lookup failed: ${err instanceof Error ? (err as Error).message : String(err)}`);
499
510
  cachedRtkLabel = null;
500
511
  }
501
512
  };
@@ -519,7 +530,9 @@ export function updateProgressWidget(
519
530
  }
520
531
  refreshRtkLabel();
521
532
  cachedLines = undefined;
522
- } catch { /* non-fatal */ }
533
+ } catch (err) { /* non-fatal */
534
+ logWarning("dashboard", `DB status update failed: ${err instanceof Error ? err.message : String(err)}`);
535
+ }
523
536
  }, 15_000);
524
537
 
525
538
  return {
@@ -878,3 +891,4 @@ function padToWidth(s: string, colWidth: number): string {
878
891
  if (vis >= colWidth) return truncateToWidth(s, colWidth, "…");
879
892
  return s + " ".repeat(colWidth - vis);
880
893
  }
894
+
@@ -28,7 +28,7 @@ import {
28
28
  buildSliceFileName,
29
29
  } from "./paths.js";
30
30
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
31
- import { logError } from "./workflow-logger.js";
31
+ import { logWarning, logError } from "./workflow-logger.js";
32
32
  import { join } from "node:path";
33
33
  import { hasImplementationArtifacts } from "./auto-recovery.js";
34
34
  import {
@@ -712,7 +712,9 @@ export const DISPATCH_RULES: DispatchRule[] = [
712
712
  }
713
713
  }
714
714
  }
715
- } catch { /* fall through — don't block on DB errors */ }
715
+ } catch (err) { /* fall through — don't block on DB errors */
716
+ logWarning("dispatch", `verification class check failed: ${err instanceof Error ? err.message : String(err)}`);
717
+ }
716
718
 
717
719
  return {
718
720
  action: "dispatch",
@@ -754,8 +756,9 @@ export async function resolveDispatch(
754
756
  try {
755
757
  const registry = getRegistry();
756
758
  return await registry.evaluateDispatch(ctx);
757
- } catch {
759
+ } catch (err) {
758
760
  // Registry not initialized — fall back to inline loop
761
+ logWarning("dispatch", `registry dispatch failed, falling back to inline rules: ${err instanceof Error ? err.message : String(err)}`);
759
762
  }
760
763
 
761
764
  for (const rule of DISPATCH_RULES) {
@@ -779,3 +782,4 @@ export async function resolveDispatch(
779
782
  export function getDispatchRuleNames(): string[] {
780
783
  return DISPATCH_RULES.map((r) => r.name);
781
784
  }
785
+
@@ -9,8 +9,8 @@ import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
9
9
  import type { GSDPreferences } from "./preferences.js";
10
10
  import { resolveModelWithFallbacksForUnit, resolveDynamicRoutingConfig } from "./preferences.js";
11
11
  import type { ComplexityTier } from "./complexity-classifier.js";
12
- import { classifyUnitComplexity, tierLabel, extractTaskMetadata } from "./complexity-classifier.js";
13
- import { resolveModelForComplexity, escalateTier } from "./model-router.js";
12
+ import { classifyUnitComplexity, tierLabel } from "./complexity-classifier.js";
13
+ import { resolveModelForComplexity, escalateTier, getEligibleModels, loadCapabilityOverrides } from "./model-router.js";
14
14
  import { getLedger, getProjectTotals } from "./metrics.js";
15
15
  import { unitPhaseLabel } from "./auto-dashboard.js";
16
16
 
@@ -107,27 +107,89 @@ export async function selectAndApplyModel(
107
107
  }
108
108
  }
109
109
 
110
- // Extract task metadata for capability scoring
111
- const taskMeta = unitType === "execute-task"
112
- ? extractTaskMetadata(unitId, basePath)
113
- : undefined;
114
-
115
- const routingResult = resolveModelForComplexity(
116
- classification, modelConfig, routingConfig, availableModelIds,
117
- unitType, taskMeta,
110
+ // Load user capability overrides from preferences (D-17: deep-merged with built-in profiles)
111
+ const capabilityOverrides = loadCapabilityOverrides(
112
+ (prefs as { modelOverrides?: Record<string, { capabilities?: Record<string, number> }> } | undefined) ?? {},
118
113
  );
119
114
 
115
+ // Fire before_model_select hook (ADR-004, D-03)
116
+ // Hook can override model selection entirely by returning { modelId }
117
+ let hookOverride: string | undefined;
118
+ if (routingConfig.hooks !== false) {
119
+ const eligible = getEligibleModels(
120
+ classification.tier,
121
+ availableModelIds,
122
+ routingConfig,
123
+ );
124
+ const hookResult = await pi.emitBeforeModelSelect({
125
+ unitType,
126
+ unitId,
127
+ classification: {
128
+ tier: classification.tier,
129
+ reason: classification.reason,
130
+ downgraded: classification.downgraded,
131
+ },
132
+ taskMetadata: classification.taskMetadata as Record<string, unknown> | undefined,
133
+ eligibleModels: eligible,
134
+ phaseConfig: modelConfig ? {
135
+ primary: modelConfig.primary,
136
+ fallbacks: modelConfig.fallbacks ?? [],
137
+ } : undefined,
138
+ });
139
+ if (hookResult?.modelId) {
140
+ hookOverride = hookResult.modelId;
141
+ }
142
+ }
143
+
144
+ let routingResult: ReturnType<typeof resolveModelForComplexity>;
145
+ if (hookOverride) {
146
+ // Hook override bypasses capability scoring entirely
147
+ routingResult = {
148
+ modelId: hookOverride,
149
+ fallbacks: [
150
+ ...(modelConfig?.fallbacks ?? []).filter(f => f !== hookOverride),
151
+ ...(modelConfig?.primary && modelConfig.primary !== hookOverride ? [modelConfig.primary] : []),
152
+ ],
153
+ tier: classification.tier,
154
+ wasDowngraded: hookOverride !== modelConfig?.primary,
155
+ reason: `hook override: ${hookOverride}`,
156
+ selectionMethod: "tier-only",
157
+ };
158
+ } else {
159
+ routingResult = resolveModelForComplexity(
160
+ classification,
161
+ modelConfig,
162
+ routingConfig,
163
+ availableModelIds,
164
+ unitType,
165
+ classification.taskMetadata,
166
+ capabilityOverrides,
167
+ );
168
+ }
169
+
120
170
  if (routingResult.wasDowngraded) {
121
171
  effectiveModelConfig = {
122
172
  primary: routingResult.modelId,
123
173
  fallbacks: routingResult.fallbacks,
124
174
  };
125
175
  if (verbose) {
126
- const method = routingResult.selectionMethod === "capability-scored" ? "capability-scored" : "tier-only";
127
- ctx.ui.notify(
128
- `Dynamic routing [${tierLabel(classification.tier)}]: ${routingResult.modelId} (${method} — ${classification.reason})`,
129
- "info",
130
- );
176
+ if (routingResult.selectionMethod === "capability-scored" && routingResult.capabilityScores) {
177
+ // Verbose scoring breakdown for capability-scored decisions (D-20)
178
+ const tierLbl = tierLabel(classification.tier);
179
+ const scores = Object.entries(routingResult.capabilityScores)
180
+ .sort(([, a], [, b]) => b - a)
181
+ .map(([id, score]) => `${id}: ${score.toFixed(1)}`)
182
+ .join(", ");
183
+ ctx.ui.notify(
184
+ `Dynamic routing [${tierLbl}]: ${routingResult.modelId} (capability-scored) — ${scores}`,
185
+ "info",
186
+ );
187
+ } else {
188
+ ctx.ui.notify(
189
+ `Dynamic routing [${tierLabel(classification.tier)}]: ${routingResult.modelId} (${classification.reason})`,
190
+ "info",
191
+ );
192
+ }
131
193
  }
132
194
  }
133
195
  routingTierLabel = ` [${tierLabel(classification.tier)}]`;
@@ -279,8 +279,9 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
279
279
  try {
280
280
  const { getTaskIssueNumberForCommit } = await import("../github-sync/sync.js");
281
281
  ghIssueNumber = getTaskIssueNumberForCommit(s.basePath, mid, sid, tid) ?? undefined;
282
- } catch {
282
+ } catch (err) {
283
283
  // GitHub sync not available — skip
284
+ logWarning("engine", `GitHub issue lookup failed: ${err instanceof Error ? err.message : String(err)}`);
284
285
  }
285
286
 
286
287
  taskContext = {
@@ -558,9 +559,7 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
558
559
  } catch (dbErr) {
559
560
  // DB unavailable — fail explicitly rather than silently reverting to markdown mutation.
560
561
  // Use 'gsd recover' to rebuild DB state from disk if needed.
561
- process.stderr.write(
562
- `gsd: retry state-reset failed (DB unavailable): ${(dbErr as Error).message}. Run 'gsd recover' to reconcile.\n`,
563
- );
562
+ logError("engine", `retry state-reset failed (DB unavailable): ${(dbErr as Error).message}. Run 'gsd recover' to reconcile.`);
564
563
  }
565
564
  }
566
565
 
@@ -732,3 +731,4 @@ export async function postUnitPostVerification(pctx: PostUnitContext): Promise<"
732
731
 
733
732
  return "continue";
734
733
  }
734
+