gsd-pi 2.62.0 → 2.62.1-dev.1ae2b74
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.
- package/dist/resources/extensions/ask-user-questions.js +47 -3
- package/dist/resources/extensions/gsd/auto/loop.js +8 -1
- package/dist/resources/extensions/gsd/auto/phases.js +10 -3
- package/dist/resources/extensions/gsd/auto-post-unit.js +6 -4
- package/dist/resources/extensions/gsd/auto-start.js +11 -6
- package/dist/resources/extensions/gsd/auto-timers.js +8 -2
- package/dist/resources/extensions/gsd/auto-verification.js +14 -3
- package/dist/resources/extensions/gsd/auto-worktree.js +19 -0
- package/dist/resources/extensions/gsd/auto.js +24 -0
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
- package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +11 -1
- package/dist/resources/extensions/gsd/commands-handlers.js +18 -7
- package/dist/resources/extensions/gsd/db-writer.js +64 -28
- package/dist/resources/extensions/gsd/preferences-models.js +74 -0
- package/dist/resources/extensions/gsd/preferences-skills.js +6 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/dist/resources/extensions/gsd/skill-catalog.js +6 -4
- package/dist/resources/extensions/gsd/skill-discovery.js +24 -6
- package/dist/resources/extensions/gsd/skill-health.js +7 -3
- package/dist/resources/extensions/gsd/skill-telemetry.js +5 -2
- package/dist/resources/extensions/gsd/state.js +1 -0
- package/dist/resources/extensions/gsd/tools/complete-slice.js +3 -3
- package/dist/resources/extensions/gsd/workflow-logger.js +13 -8
- package/dist/resources/extensions/gsd/workflow-reconcile.js +3 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +17 -17
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +17 -17
- package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
- package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +2 -2
- package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
- package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +2 -2
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-2f24283c162b6ab3.js → page-f2a7482d42a5614b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/{layout-9ecfd95f343793f0.js → layout-a16c7a7ecdf0c2cf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-0c485498795110d6.js +1 -0
- package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +1 -0
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +1 -0
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
- package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/package.json +1 -1
- package/packages/mcp-server/src/cli.ts +1 -1
- package/packages/mcp-server/src/index.ts +15 -1
- package/packages/mcp-server/src/readers/captures.ts +119 -0
- package/packages/mcp-server/src/readers/doctor-lite.ts +225 -0
- package/packages/mcp-server/src/readers/index.ts +16 -0
- package/packages/mcp-server/src/readers/knowledge.ts +111 -0
- package/packages/mcp-server/src/readers/metrics.ts +118 -0
- package/packages/mcp-server/src/readers/paths.ts +217 -0
- package/packages/mcp-server/src/readers/readers.test.ts +509 -0
- package/packages/mcp-server/src/readers/roadmap.ts +263 -0
- package/packages/mcp-server/src/readers/state.ts +223 -0
- package/packages/mcp-server/src/server.ts +134 -3
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts +26 -6
- package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
- package/packages/pi-ai/dist/utils/repair-tool-json.js +67 -9
- package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +73 -1
- package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
- package/packages/pi-ai/src/utils/repair-tool-json.ts +74 -10
- package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +94 -1
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +16 -0
- package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +4 -0
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +3 -0
- package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.js +48 -16
- package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js +20 -3
- package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +21 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
- package/packages/pi-coding-agent/src/core/retry-handler.test.ts +30 -3
- package/packages/pi-coding-agent/src/core/retry-handler.ts +49 -16
- package/pkg/package.json +1 -1
- package/src/resources/extensions/ask-user-questions.ts +60 -4
- package/src/resources/extensions/gsd/auto/loop.ts +8 -1
- package/src/resources/extensions/gsd/auto/phases.ts +8 -6
- package/src/resources/extensions/gsd/auto-post-unit.ts +6 -3
- package/src/resources/extensions/gsd/auto-start.ts +11 -6
- package/src/resources/extensions/gsd/auto-timers.ts +8 -2
- package/src/resources/extensions/gsd/auto-verification.ts +14 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +18 -0
- package/src/resources/extensions/gsd/auto.ts +25 -0
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
- package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +13 -1
- package/src/resources/extensions/gsd/commands-handlers.ts +20 -7
- package/src/resources/extensions/gsd/db-writer.ts +67 -30
- package/src/resources/extensions/gsd/preferences-models.ts +78 -0
- package/src/resources/extensions/gsd/preferences-skills.ts +6 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
- package/src/resources/extensions/gsd/skill-catalog.ts +6 -3
- package/src/resources/extensions/gsd/skill-discovery.ts +23 -6
- package/src/resources/extensions/gsd/skill-health.ts +7 -3
- package/src/resources/extensions/gsd/skill-telemetry.ts +5 -2
- package/src/resources/extensions/gsd/state.ts +1 -0
- package/src/resources/extensions/gsd/tests/ask-user-questions-dedup.test.ts +120 -0
- package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +22 -2
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +107 -0
- package/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts +51 -0
- package/src/resources/extensions/gsd/tests/db-writer.test.ts +41 -0
- package/src/resources/extensions/gsd/tests/model-isolation.test.ts +75 -1
- package/src/resources/extensions/gsd/tests/steer-worktree-path.test.ts +108 -0
- package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +17 -4
- package/src/resources/extensions/gsd/tests/workflow-logger.test.ts +17 -41
- package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +81 -2
- package/src/resources/extensions/gsd/tools/complete-slice.ts +3 -5
- package/src/resources/extensions/gsd/workflow-logger.ts +13 -8
- package/src/resources/extensions/gsd/workflow-reconcile.ts +3 -1
- package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +6 -1
- package/dist/web/standalone/.next/static/chunks/app/page-62be3b5fa91e4c8f.js +0 -1
- package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +0 -1
- /package/dist/web/standalone/.next/static/{F4rzqt_3m83A68ZRiU12r → erQZ_8_1lkclnPJLJnCxG}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{F4rzqt_3m83A68ZRiU12r → erQZ_8_1lkclnPJLJnCxG}/_ssgManifest.js +0 -0
|
@@ -20,7 +20,8 @@ import {
|
|
|
20
20
|
selectDoctorScope,
|
|
21
21
|
filterDoctorIssues,
|
|
22
22
|
} from "./doctor.js";
|
|
23
|
-
import { isAutoActive } from "./auto.js";
|
|
23
|
+
import { isAutoActive, checkRemoteAutoSession } from "./auto.js";
|
|
24
|
+
import { getAutoWorktreePath } from "./auto-worktree.js";
|
|
24
25
|
import { projectRoot } from "./commands/context.js";
|
|
25
26
|
import { loadPrompt } from "./prompt-loader.js";
|
|
26
27
|
|
|
@@ -222,7 +223,19 @@ export async function handleSteer(change: string, ctx: ExtensionCommandContext,
|
|
|
222
223
|
const sid = state.activeSlice?.id ?? "none";
|
|
223
224
|
const tid = state.activeTask?.id ?? "none";
|
|
224
225
|
const appliedAt = `${mid}/${sid}/${tid}`;
|
|
225
|
-
|
|
226
|
+
|
|
227
|
+
// Resolve the correct target path: only route to a worktree when auto-mode
|
|
228
|
+
// is actively running there (in-process or remote). A worktree directory may
|
|
229
|
+
// exist from a previous session without being the active runtime path —
|
|
230
|
+
// writing there without a live session would silently drop the override.
|
|
231
|
+
const autoRunning = isAutoActive() || checkRemoteAutoSession(basePath).running;
|
|
232
|
+
const wtPath = autoRunning && mid !== "none"
|
|
233
|
+
? getAutoWorktreePath(basePath, mid)
|
|
234
|
+
: null;
|
|
235
|
+
const targetPath = wtPath ?? basePath;
|
|
236
|
+
await appendOverride(targetPath, change, appliedAt);
|
|
237
|
+
|
|
238
|
+
const overrideLoc = wtPath ? "worktree `.gsd/OVERRIDES.md`" : "`.gsd/OVERRIDES.md`";
|
|
226
239
|
|
|
227
240
|
if (isAutoActive()) {
|
|
228
241
|
pi.sendMessage({
|
|
@@ -232,14 +245,14 @@ export async function handleSteer(change: string, ctx: ExtensionCommandContext,
|
|
|
232
245
|
"",
|
|
233
246
|
`**Override:** ${change}`,
|
|
234
247
|
"",
|
|
235
|
-
|
|
248
|
+
`This override has been saved to ${overrideLoc} and will be injected into all future task prompts.`,
|
|
236
249
|
"A document rewrite unit will run before the next task to propagate this change across all active plan documents.",
|
|
237
250
|
"",
|
|
238
251
|
"If you are mid-task, finish your current work respecting this override. The next dispatched unit will be a document rewrite.",
|
|
239
252
|
].join("\n"),
|
|
240
253
|
display: false,
|
|
241
254
|
}, { triggerTurn: true });
|
|
242
|
-
ctx.ui.notify(`Override registered: "${change}". Will be applied before next task dispatch.`, "info");
|
|
255
|
+
ctx.ui.notify(`Override registered (${overrideLoc}): "${change}". Will be applied before next task dispatch.`, "info");
|
|
243
256
|
} else {
|
|
244
257
|
pi.sendMessage({
|
|
245
258
|
customType: "gsd-hard-steer",
|
|
@@ -248,13 +261,13 @@ export async function handleSteer(change: string, ctx: ExtensionCommandContext,
|
|
|
248
261
|
"",
|
|
249
262
|
`**Override:** ${change}`,
|
|
250
263
|
"",
|
|
251
|
-
|
|
252
|
-
|
|
264
|
+
`This override has been saved to ${overrideLoc}.`,
|
|
265
|
+
`Before continuing, read ${overrideLoc} and update the current plan documents to reflect this change.`,
|
|
253
266
|
"Focus on: active slice plan, incomplete task plans, and DECISIONS.md.",
|
|
254
267
|
].join("\n"),
|
|
255
268
|
display: false,
|
|
256
269
|
}, { triggerTurn: true });
|
|
257
|
-
ctx.ui.notify(`Override registered: "${change}". Update plan documents to reflect this change.`, "info");
|
|
270
|
+
ctx.ui.notify(`Override registered (${overrideLoc}): "${change}". Update plan documents to reflect this change.`, "info");
|
|
258
271
|
}
|
|
259
272
|
}
|
|
260
273
|
|
|
@@ -272,6 +272,10 @@ export interface SaveRequirementFields {
|
|
|
272
272
|
/**
|
|
273
273
|
* Save a new requirement to DB and regenerate REQUIREMENTS.md.
|
|
274
274
|
* Auto-assigns the next ID via nextRequirementId().
|
|
275
|
+
*
|
|
276
|
+
* The ID computation and insert are wrapped in a single transaction
|
|
277
|
+
* to prevent parallel race conditions (same pattern as saveDecisionToDb).
|
|
278
|
+
*
|
|
275
279
|
* Returns the assigned ID.
|
|
276
280
|
*/
|
|
277
281
|
export async function saveRequirementToDb(
|
|
@@ -281,24 +285,37 @@ export async function saveRequirementToDb(
|
|
|
281
285
|
try {
|
|
282
286
|
const db = await import('./gsd-db.js');
|
|
283
287
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
288
|
+
// Atomic ID assignment + insert inside a transaction.
|
|
289
|
+
const id = db.transaction(() => {
|
|
290
|
+
const adapter = db._getAdapter();
|
|
291
|
+
if (!adapter) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
292
|
+
|
|
293
|
+
const row = adapter
|
|
294
|
+
.prepare('SELECT MAX(CAST(SUBSTR(id, 2) AS INTEGER)) as max_num FROM requirements')
|
|
295
|
+
.get();
|
|
296
|
+
const maxNum = row ? (row['max_num'] as number | null) : null;
|
|
297
|
+
const nextId = (maxNum == null || isNaN(maxNum))
|
|
298
|
+
? 'R001'
|
|
299
|
+
: `R${String(maxNum + 1).padStart(3, '0')}`;
|
|
300
|
+
|
|
301
|
+
const requirement: Requirement = {
|
|
302
|
+
id: nextId,
|
|
303
|
+
class: fields.class,
|
|
304
|
+
status: fields.status ?? 'active',
|
|
305
|
+
description: fields.description,
|
|
306
|
+
why: fields.why,
|
|
307
|
+
source: fields.source,
|
|
308
|
+
primary_owner: fields.primary_owner ?? '',
|
|
309
|
+
supporting_slices: fields.supporting_slices ?? '',
|
|
310
|
+
validation: fields.validation ?? '',
|
|
311
|
+
notes: fields.notes ?? '',
|
|
312
|
+
full_content: '',
|
|
313
|
+
superseded_by: null,
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
db.upsertRequirement(requirement);
|
|
317
|
+
return nextId;
|
|
318
|
+
});
|
|
302
319
|
|
|
303
320
|
// Fetch all requirements for full file regeneration
|
|
304
321
|
const adapter = db._getAdapter();
|
|
@@ -358,6 +375,11 @@ export interface SaveDecisionFields {
|
|
|
358
375
|
/**
|
|
359
376
|
* Save a new decision to DB and regenerate DECISIONS.md.
|
|
360
377
|
* Auto-assigns the next ID via nextDecisionId().
|
|
378
|
+
*
|
|
379
|
+
* The ID computation (SELECT MAX) and insert are wrapped in a single
|
|
380
|
+
* transaction to prevent parallel tool calls from computing the same ID
|
|
381
|
+
* and silently overwriting each other (#3326, #3339, #3459).
|
|
382
|
+
*
|
|
361
383
|
* Returns the assigned ID.
|
|
362
384
|
*/
|
|
363
385
|
export async function saveDecisionToDb(
|
|
@@ -367,18 +389,33 @@ export async function saveDecisionToDb(
|
|
|
367
389
|
try {
|
|
368
390
|
const db = await import('./gsd-db.js');
|
|
369
391
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
db.
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
392
|
+
// Atomic ID assignment + insert inside a transaction to prevent
|
|
393
|
+
// parallel calls from racing on the same MAX(id) value.
|
|
394
|
+
const id = db.transaction(() => {
|
|
395
|
+
const adapter = db._getAdapter();
|
|
396
|
+
if (!adapter) throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
|
|
397
|
+
|
|
398
|
+
const row = adapter
|
|
399
|
+
.prepare('SELECT MAX(CAST(SUBSTR(id, 2) AS INTEGER)) as max_num FROM decisions')
|
|
400
|
+
.get();
|
|
401
|
+
const maxNum = row ? (row['max_num'] as number | null) : null;
|
|
402
|
+
const nextId = (maxNum == null || isNaN(maxNum))
|
|
403
|
+
? 'D001'
|
|
404
|
+
: `D${String(maxNum + 1).padStart(3, '0')}`;
|
|
405
|
+
|
|
406
|
+
db.upsertDecision({
|
|
407
|
+
id: nextId,
|
|
408
|
+
when_context: fields.when_context ?? '',
|
|
409
|
+
scope: fields.scope,
|
|
410
|
+
decision: fields.decision,
|
|
411
|
+
choice: fields.choice,
|
|
412
|
+
rationale: fields.rationale,
|
|
413
|
+
revisable: fields.revisable ?? 'Yes',
|
|
414
|
+
made_by: fields.made_by ?? 'agent',
|
|
415
|
+
superseded_by: null,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return nextId;
|
|
382
419
|
});
|
|
383
420
|
|
|
384
421
|
// Fetch all decisions (including superseded for the full register)
|
|
@@ -107,6 +107,84 @@ export function resolveModelWithFallbacksForUnit(unitType: string): ResolvedMode
|
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Resolve the default session model from GSD preferences.
|
|
112
|
+
*
|
|
113
|
+
* Used at auto-mode bootstrap to override the session model that was
|
|
114
|
+
* determined by settings.json (defaultProvider/defaultModel). When
|
|
115
|
+
* PREFERENCES.md (or project preferences) configures an `execution` model
|
|
116
|
+
* we treat that as the session default. Falls back through execution →
|
|
117
|
+
* planning → first configured model.
|
|
118
|
+
*
|
|
119
|
+
* Accepts an optional `sessionProvider` for bare model IDs that don't
|
|
120
|
+
* include an explicit provider prefix (e.g. `gpt-5.4` instead of
|
|
121
|
+
* `openai-codex/gpt-5.4`). When a bare ID is found and sessionProvider
|
|
122
|
+
* is available, the session provider is used. Without sessionProvider,
|
|
123
|
+
* bare IDs are still returned with provider set to the bare ID itself
|
|
124
|
+
* so downstream resolution (resolveModelId) can match it.
|
|
125
|
+
*
|
|
126
|
+
* Returns `{ provider, id }` or `undefined` if no model preference is
|
|
127
|
+
* configured.
|
|
128
|
+
*/
|
|
129
|
+
export function resolveDefaultSessionModel(
|
|
130
|
+
sessionProvider?: string,
|
|
131
|
+
): { provider: string; id: string } | undefined {
|
|
132
|
+
const prefs = loadEffectiveGSDPreferences();
|
|
133
|
+
if (!prefs?.preferences.models) return undefined;
|
|
134
|
+
|
|
135
|
+
const m = prefs.preferences.models as GSDModelConfigV2;
|
|
136
|
+
|
|
137
|
+
// Priority: execution → planning → first configured value
|
|
138
|
+
const candidates: Array<string | GSDPhaseModelConfig | undefined> = [
|
|
139
|
+
m.execution,
|
|
140
|
+
m.planning,
|
|
141
|
+
m.research,
|
|
142
|
+
m.discuss,
|
|
143
|
+
m.completion,
|
|
144
|
+
m.validation,
|
|
145
|
+
m.subagent,
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
for (const cfg of candidates) {
|
|
149
|
+
if (!cfg) continue;
|
|
150
|
+
|
|
151
|
+
// Normalize to provider + id from the various config shapes
|
|
152
|
+
let provider: string | undefined;
|
|
153
|
+
let id: string;
|
|
154
|
+
|
|
155
|
+
if (typeof cfg === "string") {
|
|
156
|
+
const slashIdx = cfg.indexOf("/");
|
|
157
|
+
if (slashIdx !== -1) {
|
|
158
|
+
provider = cfg.slice(0, slashIdx);
|
|
159
|
+
id = cfg.slice(slashIdx + 1);
|
|
160
|
+
} else {
|
|
161
|
+
// Bare model ID (e.g. "gpt-5.4") — use session provider as context
|
|
162
|
+
provider = sessionProvider;
|
|
163
|
+
id = cfg;
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// Object config: { model, provider?, fallbacks? }
|
|
167
|
+
if (cfg.provider) {
|
|
168
|
+
provider = cfg.provider;
|
|
169
|
+
} else if (cfg.model.includes("/")) {
|
|
170
|
+
const slashIdx = cfg.model.indexOf("/");
|
|
171
|
+
provider = cfg.model.slice(0, slashIdx);
|
|
172
|
+
id = cfg.model.slice(slashIdx + 1);
|
|
173
|
+
return { provider, id };
|
|
174
|
+
} else {
|
|
175
|
+
provider = sessionProvider;
|
|
176
|
+
}
|
|
177
|
+
id = cfg.model;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (provider && id) {
|
|
181
|
+
return { provider, id };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
110
188
|
/**
|
|
111
189
|
* Determines the next fallback model to try when the current model fails.
|
|
112
190
|
* If the current model is not in the configured list, returns the primary model.
|
|
@@ -24,13 +24,18 @@ export type { GSDSkillRule, SkillDiscoveryMode, SkillResolution, SkillResolution
|
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Known skill directories, in priority order.
|
|
27
|
-
*
|
|
27
|
+
* Searches both the skills.sh ecosystem directory (~/.agents/skills/) and
|
|
28
|
+
* Claude Code's official directory (~/.claude/skills/). Project-level
|
|
29
|
+
* directories for both conventions are included as well.
|
|
28
30
|
* Legacy ~/.gsd/agent/skills/ is included as a fallback for pre-migration installs.
|
|
29
31
|
*/
|
|
30
32
|
export function getSkillSearchDirs(cwd: string): Array<{ dir: string; method: SkillResolution["method"] }> {
|
|
31
33
|
const dirs: Array<{ dir: string; method: SkillResolution["method"] }> = [
|
|
32
34
|
{ dir: join(homedir(), ".agents", "skills"), method: "user-skill" },
|
|
33
35
|
{ dir: join(cwd, ".agents", "skills"), method: "project-skill" },
|
|
36
|
+
// Claude Code official skill directories
|
|
37
|
+
{ dir: join(homedir(), ".claude", "skills"), method: "user-skill" },
|
|
38
|
+
{ dir: join(cwd, ".claude", "skills"), method: "project-skill" },
|
|
34
39
|
];
|
|
35
40
|
// Legacy fallback — read skills from old GSD directory only if migration hasn't completed
|
|
36
41
|
const legacyDir = join(homedir(), ".gsd", "agent", "skills");
|
|
@@ -30,7 +30,7 @@ Ask **1–3 questions per round**. Keep each question focused on one of:
|
|
|
30
30
|
- **The biggest technical unknowns / risks** — what could fail, what hasn't been proven
|
|
31
31
|
- **What external systems/services this touches** — APIs, databases, third-party services
|
|
32
32
|
|
|
33
|
-
**If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions` for each round. 1–3 questions per call, each as a separate question object. Keep option labels short (3–5 words). Always include a freeform "Other / let me explain" option. When the user picks that option or writes a long freeform answer, switch to plain text follow-up for that thread before resuming structured questions.
|
|
33
|
+
**If `{{structuredQuestionsAvailable}}` is `true`:** use `ask_user_questions` for each round. 1–3 questions per call, each as a separate question object. Keep option labels short (3–5 words). Always include a freeform "Other / let me explain" option. When the user picks that option or writes a long freeform answer, switch to plain text follow-up for that thread before resuming structured questions. **IMPORTANT: Call `ask_user_questions` exactly once per turn. Never make multiple calls with the same or overlapping questions — wait for the user's response before asking the next round.**
|
|
34
34
|
|
|
35
35
|
**If `{{structuredQuestionsAvailable}}` is `false`:** ask questions in plain text. Keep each round to 1–3 focused questions. Wait for answers before asking the next round.
|
|
36
36
|
|
|
@@ -22,7 +22,7 @@ Do **not** go deep — just enough that your questions reflect what's actually t
|
|
|
22
22
|
|
|
23
23
|
### Question rounds
|
|
24
24
|
|
|
25
|
-
Ask **1–3 questions per round** using `ask_user_questions`. Keep each question focused on one of:
|
|
25
|
+
Ask **1–3 questions per round** using `ask_user_questions`. **Call `ask_user_questions` exactly once per turn — never make multiple calls with the same or overlapping questions. Wait for the user's response before asking the next round.** Keep each question focused on one of:
|
|
26
26
|
- **UX and user-facing behaviour** — what does the user see, click, trigger, or experience?
|
|
27
27
|
- **Edge cases and failure states** — what happens when things go wrong or are in unusual states?
|
|
28
28
|
- **Scope boundaries** — what is explicitly in vs out for this slice? What deferred to later?
|
|
@@ -935,13 +935,16 @@ export async function installPacksBatched(
|
|
|
935
935
|
|
|
936
936
|
/**
|
|
937
937
|
* Check if any skills from a pack are already installed.
|
|
938
|
+
* Searches both the skills.sh ecosystem directory and Claude Code's official directory.
|
|
938
939
|
*/
|
|
939
940
|
export function isPackInstalled(pack: SkillPack): boolean {
|
|
940
|
-
const
|
|
941
|
-
|
|
941
|
+
const skillsDirs = [
|
|
942
|
+
join(homedir(), ".agents", "skills"),
|
|
943
|
+
join(homedir(), ".claude", "skills"),
|
|
944
|
+
];
|
|
942
945
|
|
|
943
946
|
return pack.skills.every((name) =>
|
|
944
|
-
existsSync(join(
|
|
947
|
+
skillsDirs.some((dir) => existsSync(join(dir, name, "SKILL.md"))),
|
|
945
948
|
);
|
|
946
949
|
}
|
|
947
950
|
|
|
@@ -12,8 +12,9 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/** Skills directories — skills.sh ecosystem + Claude Code official */
|
|
16
16
|
const SKILLS_DIR = join(homedir(), ".agents", "skills");
|
|
17
|
+
const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
|
|
17
18
|
|
|
18
19
|
export interface DiscoveredSkill {
|
|
19
20
|
name: string;
|
|
@@ -58,8 +59,9 @@ export function detectNewSkills(): DiscoveredSkill[] {
|
|
|
58
59
|
for (const dir of current) {
|
|
59
60
|
if (baselineSkills.has(dir)) continue;
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// Check both skill directories for the SKILL.md file
|
|
63
|
+
const skillMdPath = resolveSkillMdPath(dir);
|
|
64
|
+
if (!skillMdPath) continue;
|
|
63
65
|
|
|
64
66
|
const meta = parseSkillFrontmatter(skillMdPath);
|
|
65
67
|
if (meta) {
|
|
@@ -97,10 +99,10 @@ ${entries}
|
|
|
97
99
|
|
|
98
100
|
// ─── Internals ────────────────────────────────────────────────────────────────
|
|
99
101
|
|
|
100
|
-
function
|
|
101
|
-
if (!existsSync(
|
|
102
|
+
function listSkillDirsFrom(dir: string): string[] {
|
|
103
|
+
if (!existsSync(dir)) return [];
|
|
102
104
|
try {
|
|
103
|
-
return readdirSync(
|
|
105
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
104
106
|
.filter(d => d.isDirectory())
|
|
105
107
|
.map(d => d.name);
|
|
106
108
|
} catch {
|
|
@@ -108,6 +110,13 @@ function listSkillDirs(): string[] {
|
|
|
108
110
|
}
|
|
109
111
|
}
|
|
110
112
|
|
|
113
|
+
function listSkillDirs(): string[] {
|
|
114
|
+
const names = new Set<string>();
|
|
115
|
+
for (const name of listSkillDirsFrom(SKILLS_DIR)) names.add(name);
|
|
116
|
+
for (const name of listSkillDirsFrom(CLAUDE_SKILLS_DIR)) names.add(name);
|
|
117
|
+
return [...names];
|
|
118
|
+
}
|
|
119
|
+
|
|
111
120
|
function parseSkillFrontmatter(path: string): { name?: string; description?: string } | null {
|
|
112
121
|
try {
|
|
113
122
|
const content = readFileSync(path, "utf-8");
|
|
@@ -131,6 +140,14 @@ function parseSkillFrontmatter(path: string): { name?: string; description?: str
|
|
|
131
140
|
}
|
|
132
141
|
}
|
|
133
142
|
|
|
143
|
+
function resolveSkillMdPath(skillName: string): string | null {
|
|
144
|
+
for (const dir of [SKILLS_DIR, CLAUDE_SKILLS_DIR]) {
|
|
145
|
+
const candidate = join(dir, skillName, "SKILL.md");
|
|
146
|
+
if (existsSync(candidate)) return candidate;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
134
151
|
function escapeXml(text: string): string {
|
|
135
152
|
return text
|
|
136
153
|
.replace(/&/g, "&")
|
|
@@ -207,9 +207,13 @@ export function formatSkillDetail(basePath: string, skillName: string): string {
|
|
|
207
207
|
lines.push(` ${date} ${u.id.padEnd(20)} ${formatTokenCount(u.tokens.total).padStart(8)} tokens ${formatCost(u.cost)}`);
|
|
208
208
|
}
|
|
209
209
|
|
|
210
|
-
// Check for SKILL.md existence
|
|
211
|
-
const
|
|
212
|
-
|
|
210
|
+
// Check for SKILL.md existence — search both ecosystem and Claude Code directories
|
|
211
|
+
const candidatePaths = [
|
|
212
|
+
join(homedir(), ".agents", "skills", skillName, "SKILL.md"),
|
|
213
|
+
join(homedir(), ".claude", "skills", skillName, "SKILL.md"),
|
|
214
|
+
];
|
|
215
|
+
const skillPath = candidatePaths.find(p => existsSync(p));
|
|
216
|
+
if (skillPath) {
|
|
213
217
|
const stat = statSync(skillPath);
|
|
214
218
|
lines.push("");
|
|
215
219
|
lines.push(`SKILL.md: ${skillPath}`);
|
|
@@ -31,12 +31,14 @@ const activelyLoadedSkills = new Set<string>();
|
|
|
31
31
|
*/
|
|
32
32
|
export function captureAvailableSkills(): void {
|
|
33
33
|
const skillsDir = join(homedir(), ".agents", "skills");
|
|
34
|
+
const claudeSkillsDir = join(homedir(), ".claude", "skills");
|
|
34
35
|
const legacyDir = join(homedir(), ".gsd", "agent", "skills");
|
|
35
36
|
const names = listSkillNames(skillsDir);
|
|
37
|
+
const claudeNames = listSkillNames(claudeSkillsDir);
|
|
36
38
|
// Include skills still in the legacy directory only if migration hasn't completed
|
|
37
39
|
const legacyMigrated = existsSync(join(legacyDir, ".migrated-to-agents"));
|
|
38
40
|
const legacyNames = legacyMigrated ? [] : listSkillNames(legacyDir);
|
|
39
|
-
const all = new Set([...names, ...legacyNames]);
|
|
41
|
+
const all = new Set([...names, ...claudeNames, ...legacyNames]);
|
|
40
42
|
availableSkills = [...all];
|
|
41
43
|
activelyLoadedSkills.clear();
|
|
42
44
|
}
|
|
@@ -106,10 +108,11 @@ export function detectStaleSkills(
|
|
|
106
108
|
|
|
107
109
|
// Check all installed skills, not just those with usage data
|
|
108
110
|
const skillsDir = join(homedir(), ".agents", "skills");
|
|
111
|
+
const claudeSkillsDir = join(homedir(), ".claude", "skills");
|
|
109
112
|
const legacyDir = join(homedir(), ".gsd", "agent", "skills");
|
|
110
113
|
const legacyMigrated = existsSync(join(legacyDir, ".migrated-to-agents"));
|
|
111
114
|
const legacyNames = legacyMigrated ? [] : listSkillNames(legacyDir);
|
|
112
|
-
const installedSet = new Set([...listSkillNames(skillsDir), ...legacyNames]);
|
|
115
|
+
const installedSet = new Set([...listSkillNames(skillsDir), ...listSkillNames(claudeSkillsDir), ...legacyNames]);
|
|
113
116
|
const installed = [...installedSet];
|
|
114
117
|
|
|
115
118
|
for (const skill of installed) {
|
|
@@ -259,6 +259,7 @@ export async function deriveState(basePath: string): Promise<GSDState> {
|
|
|
259
259
|
_telemetry.markdownDeriveCount++;
|
|
260
260
|
}
|
|
261
261
|
} else {
|
|
262
|
+
logWarning("state", "DB unavailable — using filesystem state derivation (degraded mode)");
|
|
262
263
|
result = await _deriveStateImpl(basePath);
|
|
263
264
|
_telemetry.markdownDeriveCount++;
|
|
264
265
|
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// ask-user-questions-dedup — Regression tests for per-turn deduplication
|
|
2
|
+
//
|
|
3
|
+
// Verifies that duplicate ask_user_questions calls within a single turn
|
|
4
|
+
// return cached results instead of re-dispatching (especially to remote
|
|
5
|
+
// channels like Discord). Also verifies the strict loop guard threshold
|
|
6
|
+
// for interactive tools.
|
|
7
|
+
//
|
|
8
|
+
// Regression: duplicate questions were sent to Discord when the LLM called
|
|
9
|
+
// ask_user_questions multiple times with the same question set in one turn,
|
|
10
|
+
// causing user confusion and tool failure cascading to plain text fallback.
|
|
11
|
+
|
|
12
|
+
import { describe, test, beforeEach } from "node:test";
|
|
13
|
+
import assert from "node:assert/strict";
|
|
14
|
+
import {
|
|
15
|
+
checkToolCallLoop,
|
|
16
|
+
resetToolCallLoopGuard,
|
|
17
|
+
} from "../bootstrap/tool-call-loop-guard.ts";
|
|
18
|
+
import {
|
|
19
|
+
resetAskUserQuestionsCache,
|
|
20
|
+
questionSignature,
|
|
21
|
+
} from "../../ask-user-questions.ts";
|
|
22
|
+
|
|
23
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
// Strict loop guard: ask_user_questions blocks on 2nd identical call
|
|
25
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
+
|
|
27
|
+
describe("ask_user_questions dedup", () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
resetToolCallLoopGuard();
|
|
30
|
+
resetAskUserQuestionsCache();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("loop guard blocks 2nd identical ask_user_questions call", () => {
|
|
34
|
+
const args = { questions: [{ id: "app_coverage", question: "Which apps?" }] };
|
|
35
|
+
|
|
36
|
+
const first = checkToolCallLoop("ask_user_questions", args);
|
|
37
|
+
assert.equal(first.block, false, "First call should be allowed");
|
|
38
|
+
|
|
39
|
+
const second = checkToolCallLoop("ask_user_questions", args);
|
|
40
|
+
assert.equal(second.block, true, "2nd identical call should be blocked");
|
|
41
|
+
assert.ok(second.reason!.includes("ask_user_questions"), "Reason should name the tool");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("loop guard allows different ask_user_questions calls", () => {
|
|
45
|
+
const args1 = { questions: [{ id: "app_coverage", question: "Which apps?" }] };
|
|
46
|
+
const args2 = { questions: [{ id: "testing_focus", question: "What priority?" }] };
|
|
47
|
+
|
|
48
|
+
const first = checkToolCallLoop("ask_user_questions", args1);
|
|
49
|
+
assert.equal(first.block, false, "First call allowed");
|
|
50
|
+
|
|
51
|
+
const second = checkToolCallLoop("ask_user_questions", args2);
|
|
52
|
+
assert.equal(second.block, false, "Different question set should be allowed");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("non-interactive tools still use normal threshold of 4", () => {
|
|
56
|
+
const args = { query: "same query" };
|
|
57
|
+
|
|
58
|
+
for (let i = 1; i <= 4; i++) {
|
|
59
|
+
const result = checkToolCallLoop("web_search", args);
|
|
60
|
+
assert.equal(result.block, false, `web_search call ${i} should be allowed`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const fifth = checkToolCallLoop("web_search", args);
|
|
64
|
+
assert.equal(fifth.block, true, "5th identical web_search should be blocked");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("cache resets independently from loop guard", () => {
|
|
68
|
+
// Verify the reset function exists and is callable
|
|
69
|
+
resetAskUserQuestionsCache();
|
|
70
|
+
// No error means the cache module is properly exported and functional
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
74
|
+
// questionSignature: full-payload hashing prevents stale cache hits
|
|
75
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
76
|
+
|
|
77
|
+
test("same IDs with different question text produce different signatures", () => {
|
|
78
|
+
const q1 = [{ id: "scope", header: "Scope", question: "Which apps to cover?",
|
|
79
|
+
options: [{ label: "All", description: "Everything" }] }];
|
|
80
|
+
const q2 = [{ id: "scope", header: "Scope", question: "Which services to test?",
|
|
81
|
+
options: [{ label: "All", description: "Everything" }] }];
|
|
82
|
+
|
|
83
|
+
assert.notEqual(questionSignature(q1), questionSignature(q2),
|
|
84
|
+
"Different question text with same ID must produce different signatures");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("same IDs with different options produce different signatures", () => {
|
|
88
|
+
const q1 = [{ id: "scope", header: "Scope", question: "Pick one",
|
|
89
|
+
options: [{ label: "A", description: "Option A" }] }];
|
|
90
|
+
const q2 = [{ id: "scope", header: "Scope", question: "Pick one",
|
|
91
|
+
options: [{ label: "B", description: "Option B" }] }];
|
|
92
|
+
|
|
93
|
+
assert.notEqual(questionSignature(q1), questionSignature(q2),
|
|
94
|
+
"Different options with same ID must produce different signatures");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("identical payloads in different order produce same signature", () => {
|
|
98
|
+
const q1 = [
|
|
99
|
+
{ id: "b", header: "B", question: "Q2", options: [{ label: "X", description: "x" }] },
|
|
100
|
+
{ id: "a", header: "A", question: "Q1", options: [{ label: "Y", description: "y" }] },
|
|
101
|
+
];
|
|
102
|
+
const q2 = [
|
|
103
|
+
{ id: "a", header: "A", question: "Q1", options: [{ label: "Y", description: "y" }] },
|
|
104
|
+
{ id: "b", header: "B", question: "Q2", options: [{ label: "X", description: "x" }] },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
assert.equal(questionSignature(q1), questionSignature(q2),
|
|
108
|
+
"Same questions in different order must produce the same signature");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("allowMultiple difference produces different signature", () => {
|
|
112
|
+
const q1 = [{ id: "scope", header: "Scope", question: "Pick",
|
|
113
|
+
options: [{ label: "A", description: "a" }], allowMultiple: false }];
|
|
114
|
+
const q2 = [{ id: "scope", header: "Scope", question: "Pick",
|
|
115
|
+
options: [{ label: "A", description: "a" }], allowMultiple: true }];
|
|
116
|
+
|
|
117
|
+
assert.notEqual(questionSignature(q1), questionSignature(q2),
|
|
118
|
+
"allowMultiple difference must produce different signatures");
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -7,8 +7,10 @@ const sourcePath = join(import.meta.dirname, "..", "auto-start.ts");
|
|
|
7
7
|
const source = readFileSync(sourcePath, "utf-8");
|
|
8
8
|
|
|
9
9
|
test("bootstrapAutoSession snapshots ctx.model before guided-flow entry (#2829)", () => {
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// #3517 changed the snapshot to prefer GSD preferences, but the ordering
|
|
11
|
+
// guarantee still holds: the snapshot must be built before guided-flow.
|
|
12
|
+
const snapshotIdx = source.indexOf("const startModelSnapshot = preferredModel");
|
|
13
|
+
assert.ok(snapshotIdx > -1, "auto-start.ts should snapshot model at bootstrap start");
|
|
12
14
|
|
|
13
15
|
const firstDiscussIdx = source.indexOf('await showSmartEntry(ctx, pi, base, { step: requestedStepMode });');
|
|
14
16
|
assert.ok(firstDiscussIdx > -1, "auto-start.ts should route through showSmartEntry during guided flow");
|
|
@@ -26,3 +28,21 @@ test("bootstrapAutoSession restores autoModeStartModel from the early snapshot (
|
|
|
26
28
|
const snapshotRefIdx = source.indexOf("provider: startModelSnapshot.provider", assignmentIdx);
|
|
27
29
|
assert.ok(snapshotRefIdx > -1, "autoModeStartModel should be restored from startModelSnapshot");
|
|
28
30
|
});
|
|
31
|
+
|
|
32
|
+
test("bootstrapAutoSession prefers GSD PREFERENCES.md over settings.json for start model (#3517)", () => {
|
|
33
|
+
// resolveDefaultSessionModel() should be called before the snapshot is built
|
|
34
|
+
const preferredIdx = source.indexOf("const preferredModel = resolveDefaultSessionModel(");
|
|
35
|
+
assert.ok(preferredIdx > -1, "auto-start.ts should call resolveDefaultSessionModel()");
|
|
36
|
+
|
|
37
|
+
// Session provider should be passed for bare model ID resolution
|
|
38
|
+
const withProviderIdx = source.indexOf("resolveDefaultSessionModel(ctx.model?.provider)");
|
|
39
|
+
assert.ok(withProviderIdx > -1, "auto-start.ts should pass ctx.model?.provider for bare ID resolution");
|
|
40
|
+
|
|
41
|
+
const snapshotIdx = source.indexOf("const startModelSnapshot = preferredModel");
|
|
42
|
+
assert.ok(snapshotIdx > -1, "startModelSnapshot should use preferredModel when available");
|
|
43
|
+
|
|
44
|
+
assert.ok(
|
|
45
|
+
preferredIdx < snapshotIdx,
|
|
46
|
+
"resolveDefaultSessionModel() must be called before building startModelSnapshot",
|
|
47
|
+
);
|
|
48
|
+
});
|