gsd-pi 2.62.0-dev.f6ad485 → 2.62.1

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 (224) hide show
  1. package/dist/resources/extensions/ask-user-questions.js +47 -3
  2. package/dist/resources/extensions/gsd/auto-start.js +11 -6
  3. package/dist/resources/extensions/gsd/auto-timers.js +8 -2
  4. package/dist/resources/extensions/gsd/auto-worktree.js +19 -0
  5. package/dist/resources/extensions/gsd/auto.js +24 -0
  6. package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -0
  7. package/dist/resources/extensions/gsd/bootstrap/tool-call-loop-guard.js +11 -1
  8. package/dist/resources/extensions/gsd/db-writer.js +64 -28
  9. package/dist/resources/extensions/gsd/preferences-models.js +74 -0
  10. package/dist/resources/extensions/gsd/preferences-skills.js +6 -1
  11. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  12. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  13. package/dist/resources/extensions/gsd/skill-catalog.js +6 -4
  14. package/dist/resources/extensions/gsd/skill-discovery.js +24 -6
  15. package/dist/resources/extensions/gsd/skill-health.js +7 -3
  16. package/dist/resources/extensions/gsd/skill-telemetry.js +5 -2
  17. package/dist/web/standalone/.next/BUILD_ID +1 -1
  18. package/dist/web/standalone/.next/app-path-routes-manifest.json +21 -21
  19. package/dist/web/standalone/.next/build-manifest.json +3 -3
  20. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  21. package/dist/web/standalone/.next/required-server-files.json +4 -4
  22. package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
  23. package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  24. package/dist/web/standalone/.next/server/app/_global-error.html +2 -2
  25. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  32. package/dist/web/standalone/.next/server/app/_not-found/page.js +2 -2
  33. package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  34. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  35. package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
  36. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  37. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  38. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  39. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  40. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  41. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  42. package/dist/web/standalone/.next/server/app/api/boot/route.js +1 -1
  43. package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
  44. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
  45. package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
  46. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
  47. package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
  48. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
  49. package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
  50. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  51. package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
  52. package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
  53. package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
  54. package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
  55. package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
  56. package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
  57. package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
  58. package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
  59. package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
  60. package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
  61. package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
  62. package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
  63. package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
  64. package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
  65. package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
  66. package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
  67. package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
  68. package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
  69. package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
  71. package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
  72. package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
  73. package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
  74. package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
  75. package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
  76. package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
  77. package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
  78. package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
  79. package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
  80. package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
  81. package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
  82. package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
  83. package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
  84. package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
  85. package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
  86. package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
  87. package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
  88. package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
  89. package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
  90. package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
  91. package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
  92. package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
  93. package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
  94. package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
  95. package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
  96. package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
  97. package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
  98. package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
  99. package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
  100. package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
  101. package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
  102. package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
  103. package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
  104. package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
  105. package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
  106. package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
  107. package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
  108. package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +2 -2
  109. package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
  110. package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
  111. package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
  112. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +2 -2
  113. package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
  114. package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
  115. package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
  116. package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
  117. package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
  118. package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
  119. package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
  120. package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
  121. package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
  122. package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
  123. package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
  124. package/dist/web/standalone/.next/server/app/index.html +1 -1
  125. package/dist/web/standalone/.next/server/app/index.rsc +4 -4
  126. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
  127. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
  128. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  129. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +3 -3
  130. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  131. package/dist/web/standalone/.next/server/app/page.js +2 -2
  132. package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
  133. package/dist/web/standalone/.next/server/app-paths-manifest.json +21 -21
  134. package/dist/web/standalone/.next/server/chunks/2229.js +1 -1
  135. package/dist/web/standalone/.next/server/chunks/7471.js +3 -3
  136. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  137. package/dist/web/standalone/.next/server/middleware.js +2 -2
  138. package/dist/web/standalone/.next/server/next-font-manifest.js +1 -1
  139. package/dist/web/standalone/.next/server/next-font-manifest.json +1 -1
  140. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  141. package/dist/web/standalone/.next/server/pages/500.html +2 -2
  142. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  143. package/dist/web/standalone/.next/static/chunks/app/_not-found/{page-f2a7482d42a5614b.js → page-2f24283c162b6ab3.js} +1 -1
  144. package/dist/web/standalone/.next/static/chunks/app/{layout-a16c7a7ecdf0c2cf.js → layout-9ecfd95f343793f0.js} +1 -1
  145. package/dist/web/standalone/.next/static/chunks/app/page-62be3b5fa91e4c8f.js +1 -0
  146. package/dist/web/standalone/.next/static/chunks/main-app-d3d4c336195465f9.js +1 -0
  147. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ab5a8926e07ec673.js +1 -0
  148. package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
  149. package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
  150. package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
  151. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api.target.mk +14 -14
  152. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_except.target.mk +14 -14
  153. package/dist/web/standalone/node_modules/node-pty/node-addon-api/node_addon_api_maybe.target.mk +14 -14
  154. package/dist/web/standalone/server.js +1 -1
  155. package/package.json +1 -1
  156. package/packages/mcp-server/src/cli.ts +1 -1
  157. package/packages/mcp-server/src/index.ts +15 -1
  158. package/packages/mcp-server/src/readers/captures.ts +119 -0
  159. package/packages/mcp-server/src/readers/doctor-lite.ts +225 -0
  160. package/packages/mcp-server/src/readers/index.ts +16 -0
  161. package/packages/mcp-server/src/readers/knowledge.ts +111 -0
  162. package/packages/mcp-server/src/readers/metrics.ts +118 -0
  163. package/packages/mcp-server/src/readers/paths.ts +217 -0
  164. package/packages/mcp-server/src/readers/readers.test.ts +509 -0
  165. package/packages/mcp-server/src/readers/roadmap.ts +263 -0
  166. package/packages/mcp-server/src/readers/state.ts +223 -0
  167. package/packages/mcp-server/src/server.ts +134 -3
  168. package/packages/pi-ai/dist/utils/repair-tool-json.d.ts +26 -6
  169. package/packages/pi-ai/dist/utils/repair-tool-json.d.ts.map +1 -1
  170. package/packages/pi-ai/dist/utils/repair-tool-json.js +67 -9
  171. package/packages/pi-ai/dist/utils/repair-tool-json.js.map +1 -1
  172. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js +73 -1
  173. package/packages/pi-ai/dist/utils/tests/repair-tool-json.test.js.map +1 -1
  174. package/packages/pi-ai/src/utils/repair-tool-json.ts +74 -10
  175. package/packages/pi-ai/src/utils/tests/repair-tool-json.test.ts +94 -1
  176. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts +2 -0
  177. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.d.ts.map +1 -0
  178. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js +16 -0
  179. package/packages/pi-coding-agent/dist/core/agent-session-model-switch.test.js.map +1 -0
  180. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  181. package/packages/pi-coding-agent/dist/core/agent-session.js +4 -0
  182. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  183. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts +3 -0
  184. package/packages/pi-coding-agent/dist/core/retry-handler.d.ts.map +1 -1
  185. package/packages/pi-coding-agent/dist/core/retry-handler.js +48 -16
  186. package/packages/pi-coding-agent/dist/core/retry-handler.js.map +1 -1
  187. package/packages/pi-coding-agent/dist/core/retry-handler.test.js +20 -3
  188. package/packages/pi-coding-agent/dist/core/retry-handler.test.js.map +1 -1
  189. package/packages/pi-coding-agent/package.json +1 -1
  190. package/packages/pi-coding-agent/src/core/agent-session-model-switch.test.ts +21 -0
  191. package/packages/pi-coding-agent/src/core/agent-session.ts +4 -0
  192. package/packages/pi-coding-agent/src/core/retry-handler.test.ts +30 -3
  193. package/packages/pi-coding-agent/src/core/retry-handler.ts +49 -16
  194. package/pkg/package.json +1 -1
  195. package/src/resources/extensions/ask-user-questions.ts +60 -4
  196. package/src/resources/extensions/gsd/auto-start.ts +11 -6
  197. package/src/resources/extensions/gsd/auto-timers.ts +8 -2
  198. package/src/resources/extensions/gsd/auto-worktree.ts +18 -0
  199. package/src/resources/extensions/gsd/auto.ts +25 -0
  200. package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
  201. package/src/resources/extensions/gsd/bootstrap/tool-call-loop-guard.ts +13 -1
  202. package/src/resources/extensions/gsd/db-writer.ts +67 -30
  203. package/src/resources/extensions/gsd/preferences-models.ts +78 -0
  204. package/src/resources/extensions/gsd/preferences-skills.ts +6 -1
  205. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  206. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  207. package/src/resources/extensions/gsd/skill-catalog.ts +6 -3
  208. package/src/resources/extensions/gsd/skill-discovery.ts +23 -6
  209. package/src/resources/extensions/gsd/skill-health.ts +7 -3
  210. package/src/resources/extensions/gsd/skill-telemetry.ts +5 -2
  211. package/src/resources/extensions/gsd/tests/ask-user-questions-dedup.test.ts +120 -0
  212. package/src/resources/extensions/gsd/tests/auto-start-model-capture.test.ts +22 -2
  213. package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +107 -0
  214. package/src/resources/extensions/gsd/tests/claude-skill-dirs.test.ts +51 -0
  215. package/src/resources/extensions/gsd/tests/db-writer.test.ts +41 -0
  216. package/src/resources/extensions/gsd/tests/model-isolation.test.ts +75 -1
  217. package/src/resources/extensions/gsd/tests/tool-call-loop-guard.test.ts +17 -4
  218. package/src/resources/extensions/gsd/tests/worktree-db-respawn-truncation.test.ts +81 -2
  219. package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +6 -1
  220. package/dist/web/standalone/.next/static/chunks/app/page-0c485498795110d6.js +0 -1
  221. package/dist/web/standalone/.next/static/chunks/main-app-fdab67f7802d7832.js +0 -1
  222. package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-459824ffb8c323dd.js +0 -1
  223. /package/dist/web/standalone/.next/static/{fbkSIi4k8fmB8mi0Sq9sF → 86gWhNPP3233lZ7KPwda7}/_buildManifest.js +0 -0
  224. /package/dist/web/standalone/.next/static/{fbkSIi4k8fmB8mi0Sq9sF → 86gWhNPP3233lZ7KPwda7}/_ssgManifest.js +0 -0
@@ -33,6 +33,31 @@ const AskUserQuestionsParams = Type.Object({
33
33
  description: "Questions to show the user. Prefer 1 and do not exceed 3.",
34
34
  }),
35
35
  });
36
+ // ─── Per-turn deduplication ──────────────────────────────────────────────────
37
+ // Prevents duplicate question dispatches (especially to remote channels like
38
+ // Discord) when the LLM calls ask_user_questions multiple times with the same
39
+ // questions in a single turn. Keyed by full canonicalized payload (id, header,
40
+ // question, options, allowMultiple) — not just IDs — so that calls with the
41
+ // same IDs but different text/options are treated as distinct.
42
+ import { createHash } from "node:crypto";
43
+ const turnCache = new Map();
44
+ /** @internal Exported for testing only. */
45
+ export function questionSignature(questions) {
46
+ const canonical = questions
47
+ .map((q) => ({
48
+ id: q.id,
49
+ header: q.header,
50
+ question: q.question,
51
+ options: (q.options || []).map((o) => ({ label: o.label, description: o.description })),
52
+ allowMultiple: !!q.allowMultiple,
53
+ }))
54
+ .sort((a, b) => a.id.localeCompare(b.id));
55
+ return createHash("sha256").update(JSON.stringify(canonical)).digest("hex").slice(0, 16);
56
+ }
57
+ /** Reset the dedup cache. Called on session boundaries. */
58
+ export function resetAskUserQuestionsCache() {
59
+ turnCache.clear();
60
+ }
36
61
  // ─── Helpers ──────────────────────────────────────────────────────────────────
37
62
  const OTHER_OPTION_LABEL = "None of the above";
38
63
  function errorResult(message, questions = []) {
@@ -73,6 +98,15 @@ export default function AskUserQuestions(pi) {
73
98
  ],
74
99
  parameters: AskUserQuestionsParams,
75
100
  async execute(_toolCallId, params, signal, _onUpdate, ctx) {
101
+ // ── Per-turn dedup: return cached result for identical question sets ──
102
+ const sig = questionSignature(params.questions);
103
+ const cached = turnCache.get(sig);
104
+ if (cached) {
105
+ return {
106
+ content: [{ type: "text", text: cached.content[0].text + "\n(Returned cached answer — this question set was already asked this turn.)" }],
107
+ details: cached.details,
108
+ };
109
+ }
76
110
  // Validation
77
111
  if (params.questions.length === 0 || params.questions.length > 3) {
78
112
  return errorResult("Error: questions must contain 1-3 items", params.questions);
@@ -87,8 +121,14 @@ export default function AskUserQuestions(pi) {
87
121
  // this is a no-op when the user has not set up Slack/Discord/Telegram.
88
122
  const { tryRemoteQuestions } = await import("./remote-questions/manager.js");
89
123
  const remoteResult = await tryRemoteQuestions(params.questions, signal);
90
- if (remoteResult)
124
+ if (remoteResult) {
125
+ // Cache successful remote results to prevent duplicate Discord dispatches
126
+ const remoteDetails = remoteResult.details;
127
+ if (remoteDetails && !remoteDetails.timed_out && !remoteDetails.error) {
128
+ turnCache.set(sig, remoteResult);
129
+ }
91
130
  return { ...remoteResult, details: remoteResult.details };
131
+ }
92
132
  if (!ctx.hasUI) {
93
133
  return errorResult("Error: UI not available (non-interactive mode)", params.questions);
94
134
  }
@@ -131,7 +171,7 @@ export default function AskUserQuestions(pi) {
131
171
  { selected: a.answers.length === 1 ? a.answers[0] : a.answers, notes: "" },
132
172
  ])),
133
173
  };
134
- return {
174
+ const fallbackResult = {
135
175
  content: [{ type: "text", text: JSON.stringify({ answers }) }],
136
176
  details: {
137
177
  questions: params.questions,
@@ -139,6 +179,8 @@ export default function AskUserQuestions(pi) {
139
179
  cancelled: false,
140
180
  },
141
181
  };
182
+ turnCache.set(sig, fallbackResult);
183
+ return fallbackResult;
142
184
  }
143
185
  // Check if cancelled (empty answers = user exited)
144
186
  const hasAnswers = Object.keys(result.answers).length > 0;
@@ -148,10 +190,12 @@ export default function AskUserQuestions(pi) {
148
190
  details: { questions: params.questions, response: null, cancelled: true },
149
191
  };
150
192
  }
151
- return {
193
+ const successResult = {
152
194
  content: [{ type: "text", text: formatForLLM(result) }],
153
195
  details: { questions: params.questions, response: result, cancelled: false },
154
196
  };
197
+ turnCache.set(sig, successResult);
198
+ return successResult;
155
199
  },
156
200
  // ─── Rendering ────────────────────────────────────────────────────────
157
201
  renderCall(args, theme) {
@@ -39,6 +39,7 @@ import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, } from "node:
39
39
  import { join } from "node:path";
40
40
  import { sep as pathSep } from "node:path";
41
41
  import { resolveProjectRootDbPath } from "./bootstrap/dynamic-tools.js";
42
+ import { resolveDefaultSessionModel } from "./preferences-models.js";
42
43
  /**
43
44
  * Bootstrap a fresh auto-mode session. Handles everything from git init
44
45
  * through secrets collection, returning when ready for the first
@@ -89,12 +90,16 @@ export async function bootstrapAutoSession(s, ctx, pi, base, verboseMode, reques
89
90
  }
90
91
  // Capture the user's session model before guided-flow dispatch can apply a
91
92
  // phase-specific planning model for a discuss turn (#2829).
92
- const startModelSnapshot = ctx.model
93
- ? {
94
- provider: ctx.model.provider,
95
- id: ctx.model.id,
96
- }
97
- : null;
93
+ //
94
+ // GSD PREFERENCES.md takes priority over the session model from settings.json
95
+ // (#3517). The session model (ctx.model) comes from findInitialModel() which
96
+ // reads defaultProvider/defaultModel from ~/.gsd/agent/settings.json. When
97
+ // the user has explicit model preferences in PREFERENCES.md, those should win.
98
+ const preferredModel = resolveDefaultSessionModel(ctx.model?.provider);
99
+ const startModelSnapshot = preferredModel
100
+ ?? (ctx.model
101
+ ? { provider: ctx.model.provider, id: ctx.model.id }
102
+ : null);
98
103
  try {
99
104
  // Validate GSD_PROJECT_ID early so the user gets immediate feedback
100
105
  const customProjectId = process.env.GSD_PROJECT_ID;
@@ -92,6 +92,10 @@ export function startUnitSupervision(sctx) {
92
92
  phase: "wrapup-warning-sent",
93
93
  wrapupWarningSent: true,
94
94
  });
95
+ // Only trigger a new turn if no tools are currently in flight.
96
+ // Triggering during active tool calls causes tool results to be skipped
97
+ // with "Skipped due to queued user message", leading to provider errors (#3512).
98
+ const softTrigger = getInFlightToolCount() === 0;
95
99
  pi.sendMessage({
96
100
  customType: "gsd-auto-wrapup",
97
101
  display: s.verbose,
@@ -104,7 +108,7 @@ export function startUnitSupervision(sctx) {
104
108
  "3. mark task or slice state on disk correctly",
105
109
  "4. leave precise resume notes if anything remains unfinished",
106
110
  ].join("\n"),
107
- }, { triggerTurn: true });
111
+ }, { triggerTurn: softTrigger });
108
112
  }, softTimeoutMs);
109
113
  // ── 2. Idle watchdog ──
110
114
  s.idleWatchdogHandle = setInterval(async () => {
@@ -245,6 +249,8 @@ export function startUnitSupervision(sctx) {
245
249
  if (s.verbose) {
246
250
  ctx.ui.notify(`Context at ${contextUsage.percent}% (threshold: ${continueHereThreshold}%) — sending wrap-up signal.`, "info");
247
251
  }
252
+ // Only trigger a new turn if no tools are currently in flight (#3512).
253
+ const contextTrigger = getInFlightToolCount() === 0;
248
254
  pi.sendMessage({
249
255
  customType: "gsd-auto-wrapup",
250
256
  display: s.verbose,
@@ -258,7 +264,7 @@ export function startUnitSupervision(sctx) {
258
264
  "4. Leave precise resume notes if anything remains unfinished",
259
265
  "Do NOT start new sub-tasks or investigations.",
260
266
  ].join("\n"),
261
- }, { triggerTurn: true });
267
+ }, { triggerTurn: contextTrigger });
262
268
  if (s.continueHereHandle) {
263
269
  clearInterval(s.continueHereHandle);
264
270
  s.continueHereHandle = null;
@@ -234,10 +234,29 @@ export function syncProjectRootToWorktree(projectRoot, worktreePath_, milestoneI
234
234
  // openDatabase re-creates it, causing "no such table" failures (#2815).
235
235
  try {
236
236
  const wtDb = join(wtGsd, "gsd.db");
237
+ let deleteSidecars = false;
237
238
  if (existsSync(wtDb)) {
238
239
  const size = statSync(wtDb).size;
239
240
  if (size === 0) {
240
241
  unlinkSync(wtDb);
242
+ deleteSidecars = true;
243
+ }
244
+ }
245
+ else {
246
+ // Main DB already missing — sidecars are orphaned from a previous
247
+ // partial cleanup and must still be removed.
248
+ deleteSidecars = true;
249
+ }
250
+ // Always clean up WAL/SHM sidecar files when the main DB was deleted
251
+ // or is already missing. Orphaned WAL/SHM files cause SQLite WAL
252
+ // recovery on next open, which triggers a CPU spin on Node 24's
253
+ // node:sqlite DatabaseSync implementation (#2478).
254
+ if (deleteSidecars) {
255
+ for (const suffix of ["-wal", "-shm"]) {
256
+ const f = wtDb + suffix;
257
+ if (existsSync(f)) {
258
+ unlinkSync(f);
259
+ }
241
260
  }
242
261
  }
243
262
  }
@@ -377,6 +377,18 @@ export async function stopAuto(ctx, pi, reason) {
377
377
  catch (e) {
378
378
  debugLog("stop-cleanup-locks", { error: e instanceof Error ? e.message : String(e) });
379
379
  }
380
+ // ── Step 1b: Flush queued follow-up messages (#3512) ──
381
+ // Late async notifications (async_job_result, gsd-auto-wrapup) can trigger
382
+ // extra LLM turns after stop. Flush them the same way run-unit.ts does.
383
+ try {
384
+ const cmdCtxAny = s.cmdCtx;
385
+ if (typeof cmdCtxAny?.clearQueue === "function") {
386
+ cmdCtxAny.clearQueue();
387
+ }
388
+ }
389
+ catch (e) {
390
+ debugLog("stop-cleanup-queue", { error: e instanceof Error ? e.message : String(e) });
391
+ }
380
392
  // ── Step 2: Skill state ──
381
393
  try {
382
394
  clearSkillSnapshot();
@@ -589,6 +601,18 @@ export async function pauseAuto(ctx, _pi, _errorContext) {
589
601
  if (!s.active)
590
602
  return;
591
603
  clearUnitTimeout();
604
+ // Flush queued follow-up messages (#3512).
605
+ // Late async notifications (async_job_result, gsd-auto-wrapup) can trigger
606
+ // extra LLM turns after pause. Flush them the same way run-unit.ts does.
607
+ try {
608
+ const cmdCtxAny = s.cmdCtx;
609
+ if (typeof cmdCtxAny?.clearQueue === "function") {
610
+ cmdCtxAny.clearQueue();
611
+ }
612
+ }
613
+ catch (e) {
614
+ debugLog("pause-cleanup-queue", { error: e instanceof Error ? e.message : String(e) });
615
+ }
592
616
  // Unblock any pending unit promise so the auto-loop is not orphaned.
593
617
  // Pass errorContext so runUnitPhase can distinguish user-initiated pause
594
618
  // from provider-error pause and avoid hard-stopping (#2762).
@@ -14,6 +14,7 @@ import { getAutoDashboardData, isAutoActive, isAutoPaused, markToolEnd, markTool
14
14
  import { isParallelActive, shutdownParallel } from "../parallel-orchestrator.js";
15
15
  import { checkToolCallLoop, resetToolCallLoopGuard } from "./tool-call-loop-guard.js";
16
16
  import { saveActivityLog } from "../activity-log.js";
17
+ import { resetAskUserQuestionsCache } from "../../ask-user-questions.js";
17
18
  // Skip the welcome screen on the very first session_start — cli.ts already
18
19
  // printed it before the TUI launched. Only re-print on /clear (subsequent sessions).
19
20
  let isFirstSession = true;
@@ -25,6 +26,7 @@ export function registerHooks(pi) {
25
26
  pi.on("session_start", async (_event, ctx) => {
26
27
  resetWriteGateState();
27
28
  resetToolCallLoopGuard();
29
+ resetAskUserQuestionsCache();
28
30
  await syncServiceTierStatus(ctx);
29
31
  // Apply show_token_cost preference (#1515)
30
32
  try {
@@ -60,6 +62,7 @@ export function registerHooks(pi) {
60
62
  pi.on("session_switch", async (_event, ctx) => {
61
63
  resetWriteGateState();
62
64
  resetToolCallLoopGuard();
65
+ resetAskUserQuestionsCache();
63
66
  clearDiscussionFlowState();
64
67
  await syncServiceTierStatus(ctx);
65
68
  loadToolApiKeys();
@@ -69,6 +72,7 @@ export function registerHooks(pi) {
69
72
  });
70
73
  pi.on("agent_end", async (event, ctx) => {
71
74
  resetToolCallLoopGuard();
75
+ resetAskUserQuestionsCache();
72
76
  await handleAgentEnd(pi, event, ctx);
73
77
  });
74
78
  // Squash-merge quick-task branch back to the original branch after the
@@ -13,8 +13,12 @@
13
13
  */
14
14
  import { createHash } from "node:crypto";
15
15
  const MAX_CONSECUTIVE_IDENTICAL_CALLS = 4;
16
+ /** Interactive/user-facing tools where even 1 duplicate is confusing. */
17
+ const STRICT_LOOP_TOOLS = new Set(["ask_user_questions"]);
18
+ const MAX_CONSECUTIVE_STRICT = 1;
16
19
  let consecutiveCount = 0;
17
20
  let lastSignature = "";
21
+ let lastToolName = "";
18
22
  let enabled = true;
19
23
  /** Hash tool name + args into a compact signature for comparison. */
20
24
  function hashToolCall(toolName, args) {
@@ -45,8 +49,12 @@ export function checkToolCallLoop(toolName, args) {
45
49
  else {
46
50
  consecutiveCount = 1;
47
51
  lastSignature = sig;
52
+ lastToolName = toolName;
48
53
  }
49
- if (consecutiveCount > MAX_CONSECUTIVE_IDENTICAL_CALLS) {
54
+ const threshold = STRICT_LOOP_TOOLS.has(toolName)
55
+ ? MAX_CONSECUTIVE_STRICT
56
+ : MAX_CONSECUTIVE_IDENTICAL_CALLS;
57
+ if (consecutiveCount > threshold) {
50
58
  return {
51
59
  block: true,
52
60
  reason: `Tool loop detected: ${toolName} called ${consecutiveCount} times ` +
@@ -61,6 +69,7 @@ export function checkToolCallLoop(toolName, args) {
61
69
  export function resetToolCallLoopGuard() {
62
70
  consecutiveCount = 0;
63
71
  lastSignature = "";
72
+ lastToolName = "";
64
73
  enabled = true;
65
74
  }
66
75
  /** Disable the guard (e.g. during shutdown). */
@@ -68,6 +77,7 @@ export function disableToolCallLoopGuard() {
68
77
  enabled = false;
69
78
  consecutiveCount = 0;
70
79
  lastSignature = "";
80
+ lastToolName = "";
71
81
  }
72
82
  /** Get current consecutive count for diagnostics. */
73
83
  export function getToolCallLoopCount() {
@@ -11,7 +11,7 @@ import { resolve } from 'node:path';
11
11
  import { readFileSync, existsSync, statSync } from 'node:fs';
12
12
  import { resolveGsdRootFile } from './paths.js';
13
13
  import { saveFile } from './files.js';
14
- import { GSDError, GSD_IO_ERROR } from './errors.js';
14
+ import { GSDError, GSD_STALE_STATE, GSD_IO_ERROR } from './errors.js';
15
15
  import { logWarning, logError } from './workflow-logger.js';
16
16
  import { invalidateStateCache } from './state.js';
17
17
  import { clearPathCache } from './paths.js';
@@ -234,27 +234,44 @@ export async function nextRequirementId() {
234
234
  /**
235
235
  * Save a new requirement to DB and regenerate REQUIREMENTS.md.
236
236
  * Auto-assigns the next ID via nextRequirementId().
237
+ *
238
+ * The ID computation and insert are wrapped in a single transaction
239
+ * to prevent parallel race conditions (same pattern as saveDecisionToDb).
240
+ *
237
241
  * Returns the assigned ID.
238
242
  */
239
243
  export async function saveRequirementToDb(fields, basePath) {
240
244
  try {
241
245
  const db = await import('./gsd-db.js');
242
- const id = await nextRequirementId();
243
- const requirement = {
244
- id,
245
- class: fields.class,
246
- status: fields.status ?? 'active',
247
- description: fields.description,
248
- why: fields.why,
249
- source: fields.source,
250
- primary_owner: fields.primary_owner ?? '',
251
- supporting_slices: fields.supporting_slices ?? '',
252
- validation: fields.validation ?? '',
253
- notes: fields.notes ?? '',
254
- full_content: '',
255
- superseded_by: null,
256
- };
257
- db.upsertRequirement(requirement);
246
+ // Atomic ID assignment + insert inside a transaction.
247
+ const id = db.transaction(() => {
248
+ const adapter = db._getAdapter();
249
+ if (!adapter)
250
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
251
+ const row = adapter
252
+ .prepare('SELECT MAX(CAST(SUBSTR(id, 2) AS INTEGER)) as max_num FROM requirements')
253
+ .get();
254
+ const maxNum = row ? row['max_num'] : null;
255
+ const nextId = (maxNum == null || isNaN(maxNum))
256
+ ? 'R001'
257
+ : `R${String(maxNum + 1).padStart(3, '0')}`;
258
+ const requirement = {
259
+ id: nextId,
260
+ class: fields.class,
261
+ status: fields.status ?? 'active',
262
+ description: fields.description,
263
+ why: fields.why,
264
+ source: fields.source,
265
+ primary_owner: fields.primary_owner ?? '',
266
+ supporting_slices: fields.supporting_slices ?? '',
267
+ validation: fields.validation ?? '',
268
+ notes: fields.notes ?? '',
269
+ full_content: '',
270
+ superseded_by: null,
271
+ };
272
+ db.upsertRequirement(requirement);
273
+ return nextId;
274
+ });
258
275
  // Fetch all requirements for full file regeneration
259
276
  const adapter = db._getAdapter();
260
277
  let allRequirements = [];
@@ -300,22 +317,41 @@ export async function saveRequirementToDb(fields, basePath) {
300
317
  /**
301
318
  * Save a new decision to DB and regenerate DECISIONS.md.
302
319
  * Auto-assigns the next ID via nextDecisionId().
320
+ *
321
+ * The ID computation (SELECT MAX) and insert are wrapped in a single
322
+ * transaction to prevent parallel tool calls from computing the same ID
323
+ * and silently overwriting each other (#3326, #3339, #3459).
324
+ *
303
325
  * Returns the assigned ID.
304
326
  */
305
327
  export async function saveDecisionToDb(fields, basePath) {
306
328
  try {
307
329
  const db = await import('./gsd-db.js');
308
- const id = await nextDecisionId();
309
- db.upsertDecision({
310
- id,
311
- when_context: fields.when_context ?? '',
312
- scope: fields.scope,
313
- decision: fields.decision,
314
- choice: fields.choice,
315
- rationale: fields.rationale,
316
- revisable: fields.revisable ?? 'Yes',
317
- made_by: fields.made_by ?? 'agent',
318
- superseded_by: null,
330
+ // Atomic ID assignment + insert inside a transaction to prevent
331
+ // parallel calls from racing on the same MAX(id) value.
332
+ const id = db.transaction(() => {
333
+ const adapter = db._getAdapter();
334
+ if (!adapter)
335
+ throw new GSDError(GSD_STALE_STATE, "gsd-db: No database open");
336
+ const row = adapter
337
+ .prepare('SELECT MAX(CAST(SUBSTR(id, 2) AS INTEGER)) as max_num FROM decisions')
338
+ .get();
339
+ const maxNum = row ? row['max_num'] : null;
340
+ const nextId = (maxNum == null || isNaN(maxNum))
341
+ ? 'D001'
342
+ : `D${String(maxNum + 1).padStart(3, '0')}`;
343
+ db.upsertDecision({
344
+ id: nextId,
345
+ when_context: fields.when_context ?? '',
346
+ scope: fields.scope,
347
+ decision: fields.decision,
348
+ choice: fields.choice,
349
+ rationale: fields.rationale,
350
+ revisable: fields.revisable ?? 'Yes',
351
+ made_by: fields.made_by ?? 'agent',
352
+ superseded_by: null,
353
+ });
354
+ return nextId;
319
355
  });
320
356
  // Fetch all decisions (including superseded for the full register)
321
357
  const adapter = db._getAdapter();
@@ -87,6 +87,80 @@ export function resolveModelWithFallbacksForUnit(unitType) {
87
87
  fallbacks: phaseConfig.fallbacks ?? [],
88
88
  };
89
89
  }
90
+ /**
91
+ * Resolve the default session model from GSD preferences.
92
+ *
93
+ * Used at auto-mode bootstrap to override the session model that was
94
+ * determined by settings.json (defaultProvider/defaultModel). When
95
+ * PREFERENCES.md (or project preferences) configures an `execution` model
96
+ * we treat that as the session default. Falls back through execution →
97
+ * planning → first configured model.
98
+ *
99
+ * Accepts an optional `sessionProvider` for bare model IDs that don't
100
+ * include an explicit provider prefix (e.g. `gpt-5.4` instead of
101
+ * `openai-codex/gpt-5.4`). When a bare ID is found and sessionProvider
102
+ * is available, the session provider is used. Without sessionProvider,
103
+ * bare IDs are still returned with provider set to the bare ID itself
104
+ * so downstream resolution (resolveModelId) can match it.
105
+ *
106
+ * Returns `{ provider, id }` or `undefined` if no model preference is
107
+ * configured.
108
+ */
109
+ export function resolveDefaultSessionModel(sessionProvider) {
110
+ const prefs = loadEffectiveGSDPreferences();
111
+ if (!prefs?.preferences.models)
112
+ return undefined;
113
+ const m = prefs.preferences.models;
114
+ // Priority: execution → planning → first configured value
115
+ const candidates = [
116
+ m.execution,
117
+ m.planning,
118
+ m.research,
119
+ m.discuss,
120
+ m.completion,
121
+ m.validation,
122
+ m.subagent,
123
+ ];
124
+ for (const cfg of candidates) {
125
+ if (!cfg)
126
+ continue;
127
+ // Normalize to provider + id from the various config shapes
128
+ let provider;
129
+ let id;
130
+ if (typeof cfg === "string") {
131
+ const slashIdx = cfg.indexOf("/");
132
+ if (slashIdx !== -1) {
133
+ provider = cfg.slice(0, slashIdx);
134
+ id = cfg.slice(slashIdx + 1);
135
+ }
136
+ else {
137
+ // Bare model ID (e.g. "gpt-5.4") — use session provider as context
138
+ provider = sessionProvider;
139
+ id = cfg;
140
+ }
141
+ }
142
+ else {
143
+ // Object config: { model, provider?, fallbacks? }
144
+ if (cfg.provider) {
145
+ provider = cfg.provider;
146
+ }
147
+ else if (cfg.model.includes("/")) {
148
+ const slashIdx = cfg.model.indexOf("/");
149
+ provider = cfg.model.slice(0, slashIdx);
150
+ id = cfg.model.slice(slashIdx + 1);
151
+ return { provider, id };
152
+ }
153
+ else {
154
+ provider = sessionProvider;
155
+ }
156
+ id = cfg.model;
157
+ }
158
+ if (provider && id) {
159
+ return { provider, id };
160
+ }
161
+ }
162
+ return undefined;
163
+ }
90
164
  /**
91
165
  * Determines the next fallback model to try when the current model fails.
92
166
  * If the current model is not in the configured list, returns the primary model.
@@ -12,13 +12,18 @@ import { validatePreferences } from "./preferences-validation.js";
12
12
  import { loadEffectiveGSDPreferences } from "./preferences.js";
13
13
  /**
14
14
  * Known skill directories, in priority order.
15
- * Global skills (~/.agents/skills/) take precedence over project skills.
15
+ * Searches both the skills.sh ecosystem directory (~/.agents/skills/) and
16
+ * Claude Code's official directory (~/.claude/skills/). Project-level
17
+ * directories for both conventions are included as well.
16
18
  * Legacy ~/.gsd/agent/skills/ is included as a fallback for pre-migration installs.
17
19
  */
18
20
  export function getSkillSearchDirs(cwd) {
19
21
  const dirs = [
20
22
  { dir: join(homedir(), ".agents", "skills"), method: "user-skill" },
21
23
  { dir: join(cwd, ".agents", "skills"), method: "project-skill" },
24
+ // Claude Code official skill directories
25
+ { dir: join(homedir(), ".claude", "skills"), method: "user-skill" },
26
+ { dir: join(cwd, ".claude", "skills"), method: "project-skill" },
22
27
  ];
23
28
  // Legacy fallback — read skills from old GSD directory only if migration hasn't completed
24
29
  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?
@@ -887,12 +887,14 @@ export async function installPacksBatched(packs, onProgress) {
887
887
  }
888
888
  /**
889
889
  * Check if any skills from a pack are already installed.
890
+ * Searches both the skills.sh ecosystem directory and Claude Code's official directory.
890
891
  */
891
892
  export function isPackInstalled(pack) {
892
- const skillsDir = join(homedir(), ".agents", "skills");
893
- if (!existsSync(skillsDir))
894
- return false;
895
- return pack.skills.every((name) => existsSync(join(skillsDir, name, "SKILL.md")));
893
+ const skillsDirs = [
894
+ join(homedir(), ".agents", "skills"),
895
+ join(homedir(), ".claude", "skills"),
896
+ ];
897
+ return pack.skills.every((name) => skillsDirs.some((dir) => existsSync(join(dir, name, "SKILL.md"))));
896
898
  }
897
899
  // ─── Init Wizard Integration ──────────────────────────────────────────────────
898
900
  /**
@@ -10,8 +10,9 @@
10
10
  import { existsSync, readdirSync, readFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { homedir } from "node:os";
13
- /** Industry-standard skills.sh global skills directory */
13
+ /** Skills directories — skills.sh ecosystem + Claude Code official */
14
14
  const SKILLS_DIR = join(homedir(), ".agents", "skills");
15
+ const CLAUDE_SKILLS_DIR = join(homedir(), ".claude", "skills");
15
16
  /** Snapshot of skill names at auto-mode start */
16
17
  let baselineSkills = null;
17
18
  /**
@@ -44,8 +45,9 @@ export function detectNewSkills() {
44
45
  for (const dir of current) {
45
46
  if (baselineSkills.has(dir))
46
47
  continue;
47
- const skillMdPath = join(SKILLS_DIR, dir, "SKILL.md");
48
- if (!existsSync(skillMdPath))
48
+ // Check both skill directories for the SKILL.md file
49
+ const skillMdPath = resolveSkillMdPath(dir);
50
+ if (!skillMdPath)
49
51
  continue;
50
52
  const meta = parseSkillFrontmatter(skillMdPath);
51
53
  if (meta) {
@@ -78,11 +80,11 @@ ${entries}
78
80
  </newly_discovered_skills>`;
79
81
  }
80
82
  // ─── Internals ────────────────────────────────────────────────────────────────
81
- function listSkillDirs() {
82
- if (!existsSync(SKILLS_DIR))
83
+ function listSkillDirsFrom(dir) {
84
+ if (!existsSync(dir))
83
85
  return [];
84
86
  try {
85
- return readdirSync(SKILLS_DIR, { withFileTypes: true })
87
+ return readdirSync(dir, { withFileTypes: true })
86
88
  .filter(d => d.isDirectory())
87
89
  .map(d => d.name);
88
90
  }
@@ -90,6 +92,14 @@ function listSkillDirs() {
90
92
  return [];
91
93
  }
92
94
  }
95
+ function listSkillDirs() {
96
+ const names = new Set();
97
+ for (const name of listSkillDirsFrom(SKILLS_DIR))
98
+ names.add(name);
99
+ for (const name of listSkillDirsFrom(CLAUDE_SKILLS_DIR))
100
+ names.add(name);
101
+ return [...names];
102
+ }
93
103
  function parseSkillFrontmatter(path) {
94
104
  try {
95
105
  const content = readFileSync(path, "utf-8");
@@ -113,6 +123,14 @@ function parseSkillFrontmatter(path) {
113
123
  return null;
114
124
  }
115
125
  }
126
+ function resolveSkillMdPath(skillName) {
127
+ for (const dir of [SKILLS_DIR, CLAUDE_SKILLS_DIR]) {
128
+ const candidate = join(dir, skillName, "SKILL.md");
129
+ if (existsSync(candidate))
130
+ return candidate;
131
+ }
132
+ return null;
133
+ }
116
134
  function escapeXml(text) {
117
135
  return text
118
136
  .replace(/&/g, "&amp;")