gsd-pi 2.10.12 → 2.12.0

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 (504) hide show
  1. package/dist/bundled-extension-paths.d.ts +2 -0
  2. package/dist/bundled-extension-paths.js +10 -0
  3. package/dist/loader.js +45 -28
  4. package/dist/onboarding.js +100 -0
  5. package/dist/resource-loader.js +12 -3
  6. package/dist/resources/GSD-WORKFLOW.md +661 -0
  7. package/dist/resources/agents/researcher.md +29 -0
  8. package/dist/resources/agents/scout.md +56 -0
  9. package/dist/resources/agents/worker.md +31 -0
  10. package/dist/resources/extensions/ask-user-questions.ts +289 -0
  11. package/dist/resources/extensions/async-jobs/async-bash-tool.ts +211 -0
  12. package/dist/resources/extensions/async-jobs/await-tool.ts +101 -0
  13. package/dist/resources/extensions/async-jobs/cancel-job-tool.ts +34 -0
  14. package/dist/resources/extensions/async-jobs/index.ts +133 -0
  15. package/dist/resources/extensions/async-jobs/job-manager.ts +211 -0
  16. package/dist/resources/extensions/bg-shell/index.ts +3179 -0
  17. package/dist/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
  18. package/dist/resources/extensions/browser-tools/capture.ts +165 -0
  19. package/dist/resources/extensions/browser-tools/core.d.ts +205 -0
  20. package/dist/resources/extensions/browser-tools/core.js +1057 -0
  21. package/dist/resources/extensions/browser-tools/evaluate-helpers.ts +184 -0
  22. package/dist/resources/extensions/browser-tools/index.ts +51 -0
  23. package/dist/resources/extensions/browser-tools/lifecycle.ts +270 -0
  24. package/dist/resources/extensions/browser-tools/package.json +24 -0
  25. package/dist/resources/extensions/browser-tools/refs.ts +264 -0
  26. package/dist/resources/extensions/browser-tools/settle.ts +197 -0
  27. package/dist/resources/extensions/browser-tools/state.ts +408 -0
  28. package/dist/resources/extensions/browser-tools/tests/browser-tools-integration.test.mjs +652 -0
  29. package/dist/resources/extensions/browser-tools/tests/browser-tools-unit.test.cjs +614 -0
  30. package/dist/resources/extensions/browser-tools/tools/assertions.ts +342 -0
  31. package/dist/resources/extensions/browser-tools/tools/forms.ts +801 -0
  32. package/dist/resources/extensions/browser-tools/tools/inspection.ts +492 -0
  33. package/dist/resources/extensions/browser-tools/tools/intent.ts +614 -0
  34. package/dist/resources/extensions/browser-tools/tools/interaction.ts +865 -0
  35. package/dist/resources/extensions/browser-tools/tools/navigation.ts +232 -0
  36. package/dist/resources/extensions/browser-tools/tools/pages.ts +303 -0
  37. package/dist/resources/extensions/browser-tools/tools/refs.ts +541 -0
  38. package/dist/resources/extensions/browser-tools/tools/screenshot.ts +83 -0
  39. package/dist/resources/extensions/browser-tools/tools/session.ts +400 -0
  40. package/dist/resources/extensions/browser-tools/tools/wait.ts +247 -0
  41. package/dist/resources/extensions/browser-tools/utils.ts +660 -0
  42. package/dist/resources/extensions/context7/index.ts +428 -0
  43. package/dist/resources/extensions/context7/package.json +11 -0
  44. package/dist/resources/extensions/get-secrets-from-user.ts +625 -0
  45. package/dist/resources/extensions/google-search/index.ts +336 -0
  46. package/dist/resources/extensions/google-search/package.json +9 -0
  47. package/dist/resources/extensions/gsd/activity-log.ts +116 -0
  48. package/dist/resources/extensions/gsd/auto.ts +3732 -0
  49. package/dist/resources/extensions/gsd/commands.ts +544 -0
  50. package/dist/resources/extensions/gsd/crash-recovery.ts +105 -0
  51. package/dist/resources/extensions/gsd/dashboard-overlay.ts +564 -0
  52. package/dist/resources/extensions/gsd/dispatch-guard.ts +78 -0
  53. package/dist/resources/extensions/gsd/docs/preferences-reference.md +278 -0
  54. package/dist/resources/extensions/gsd/doctor.ts +766 -0
  55. package/dist/resources/extensions/gsd/exit-command.ts +18 -0
  56. package/dist/resources/extensions/gsd/files.ts +857 -0
  57. package/dist/resources/extensions/gsd/git-service.ts +904 -0
  58. package/dist/resources/extensions/gsd/gitignore.ts +200 -0
  59. package/dist/resources/extensions/gsd/guided-flow.ts +1054 -0
  60. package/dist/resources/extensions/gsd/index.ts +735 -0
  61. package/dist/resources/extensions/gsd/metrics.ts +374 -0
  62. package/dist/resources/extensions/gsd/migrate/command.ts +218 -0
  63. package/dist/resources/extensions/gsd/migrate/index.ts +42 -0
  64. package/dist/resources/extensions/gsd/migrate/parser.ts +323 -0
  65. package/dist/resources/extensions/gsd/migrate/parsers.ts +624 -0
  66. package/dist/resources/extensions/gsd/migrate/preview.ts +48 -0
  67. package/dist/resources/extensions/gsd/migrate/transformer.ts +346 -0
  68. package/dist/resources/extensions/gsd/migrate/types.ts +370 -0
  69. package/dist/resources/extensions/gsd/migrate/validator.ts +55 -0
  70. package/dist/resources/extensions/gsd/migrate/writer.ts +539 -0
  71. package/dist/resources/extensions/gsd/native-git-bridge.ts +181 -0
  72. package/dist/resources/extensions/gsd/native-parser-bridge.ts +135 -0
  73. package/dist/resources/extensions/gsd/observability-validator.ts +408 -0
  74. package/dist/resources/extensions/gsd/package.json +11 -0
  75. package/dist/resources/extensions/gsd/paths.ts +339 -0
  76. package/dist/resources/extensions/gsd/post-unit-hooks.ts +449 -0
  77. package/dist/resources/extensions/gsd/preferences.ts +988 -0
  78. package/dist/resources/extensions/gsd/prompt-loader.ts +92 -0
  79. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
  80. package/dist/resources/extensions/gsd/prompts/complete-slice.md +27 -0
  81. package/dist/resources/extensions/gsd/prompts/discuss.md +237 -0
  82. package/dist/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
  83. package/dist/resources/extensions/gsd/prompts/execute-task.md +63 -0
  84. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -0
  85. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -0
  86. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +61 -0
  87. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +3 -0
  88. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +29 -0
  89. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -0
  90. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +13 -0
  91. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
  92. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +78 -0
  93. package/dist/resources/extensions/gsd/prompts/plan-slice.md +55 -0
  94. package/dist/resources/extensions/gsd/prompts/queue.md +100 -0
  95. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
  96. package/dist/resources/extensions/gsd/prompts/replan-slice.md +39 -0
  97. package/dist/resources/extensions/gsd/prompts/research-milestone.md +37 -0
  98. package/dist/resources/extensions/gsd/prompts/research-slice.md +28 -0
  99. package/dist/resources/extensions/gsd/prompts/review-migration.md +66 -0
  100. package/dist/resources/extensions/gsd/prompts/run-uat.md +109 -0
  101. package/dist/resources/extensions/gsd/prompts/system.md +190 -0
  102. package/dist/resources/extensions/gsd/prompts/worktree-merge.md +123 -0
  103. package/dist/resources/extensions/gsd/roadmap-slices.ts +50 -0
  104. package/dist/resources/extensions/gsd/session-forensics.ts +500 -0
  105. package/dist/resources/extensions/gsd/skill-discovery.ts +137 -0
  106. package/dist/resources/extensions/gsd/state.ts +569 -0
  107. package/dist/resources/extensions/gsd/templates/context.md +76 -0
  108. package/dist/resources/extensions/gsd/templates/decisions.md +8 -0
  109. package/dist/resources/extensions/gsd/templates/milestone-summary.md +73 -0
  110. package/dist/resources/extensions/gsd/templates/plan.md +131 -0
  111. package/dist/resources/extensions/gsd/templates/preferences.md +23 -0
  112. package/dist/resources/extensions/gsd/templates/project.md +31 -0
  113. package/dist/resources/extensions/gsd/templates/reassessment.md +28 -0
  114. package/dist/resources/extensions/gsd/templates/requirements.md +81 -0
  115. package/dist/resources/extensions/gsd/templates/research.md +46 -0
  116. package/dist/resources/extensions/gsd/templates/roadmap.md +118 -0
  117. package/dist/resources/extensions/gsd/templates/secrets-manifest.md +22 -0
  118. package/dist/resources/extensions/gsd/templates/slice-context.md +58 -0
  119. package/dist/resources/extensions/gsd/templates/slice-summary.md +99 -0
  120. package/dist/resources/extensions/gsd/templates/state.md +19 -0
  121. package/dist/resources/extensions/gsd/templates/task-plan.md +52 -0
  122. package/dist/resources/extensions/gsd/templates/task-summary.md +57 -0
  123. package/dist/resources/extensions/gsd/templates/uat.md +54 -0
  124. package/dist/resources/extensions/gsd/tests/activity-log-prune.test.ts +297 -0
  125. package/dist/resources/extensions/gsd/tests/activity-log-save.test.ts +127 -0
  126. package/dist/resources/extensions/gsd/tests/auto-draft-pause.test.ts +109 -0
  127. package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +45 -0
  128. package/dist/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +196 -0
  129. package/dist/resources/extensions/gsd/tests/auto-supervisor.test.mjs +53 -0
  130. package/dist/resources/extensions/gsd/tests/collect-from-manifest.test.ts +469 -0
  131. package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +200 -0
  132. package/dist/resources/extensions/gsd/tests/cost-projection.test.ts +134 -0
  133. package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +312 -0
  134. package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +299 -0
  135. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +660 -0
  136. package/dist/resources/extensions/gsd/tests/discuss-prompt.test.ts +27 -0
  137. package/dist/resources/extensions/gsd/tests/dispatch-guard.test.ts +91 -0
  138. package/dist/resources/extensions/gsd/tests/doctor-fixlevel.test.ts +170 -0
  139. package/dist/resources/extensions/gsd/tests/doctor.test.ts +594 -0
  140. package/dist/resources/extensions/gsd/tests/draft-promotion.test.ts +169 -0
  141. package/dist/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
  142. package/dist/resources/extensions/gsd/tests/git-service.test.ts +1788 -0
  143. package/dist/resources/extensions/gsd/tests/idle-recovery.test.ts +577 -0
  144. package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +580 -0
  145. package/dist/resources/extensions/gsd/tests/manifest-status.test.ts +283 -0
  146. package/dist/resources/extensions/gsd/tests/metrics-io.test.ts +176 -0
  147. package/dist/resources/extensions/gsd/tests/metrics.test.ts +188 -0
  148. package/dist/resources/extensions/gsd/tests/migrate-command.test.ts +369 -0
  149. package/dist/resources/extensions/gsd/tests/migrate-parser.test.ts +757 -0
  150. package/dist/resources/extensions/gsd/tests/migrate-transformer.test.ts +635 -0
  151. package/dist/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +414 -0
  152. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +297 -0
  153. package/dist/resources/extensions/gsd/tests/migrate-writer.test.ts +398 -0
  154. package/dist/resources/extensions/gsd/tests/must-have-parser.test.ts +291 -0
  155. package/dist/resources/extensions/gsd/tests/network-error-fallback.test.ts +54 -0
  156. package/dist/resources/extensions/gsd/tests/next-milestone-id.test.ts +66 -0
  157. package/dist/resources/extensions/gsd/tests/orphaned-branch.test.ts +353 -0
  158. package/dist/resources/extensions/gsd/tests/parsers.test.ts +1664 -0
  159. package/dist/resources/extensions/gsd/tests/plan-milestone.test.ts +133 -0
  160. package/dist/resources/extensions/gsd/tests/plan-quality-validator.test.ts +363 -0
  161. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
  162. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
  163. package/dist/resources/extensions/gsd/tests/queue-draft-detection.test.ts +126 -0
  164. package/dist/resources/extensions/gsd/tests/reassess-prompt.test.ts +142 -0
  165. package/dist/resources/extensions/gsd/tests/regex-hardening.test.ts +281 -0
  166. package/dist/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
  167. package/dist/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
  168. package/dist/resources/extensions/gsd/tests/replan-slice.test.ts +493 -0
  169. package/dist/resources/extensions/gsd/tests/requirements.test.ts +106 -0
  170. package/dist/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +35 -0
  171. package/dist/resources/extensions/gsd/tests/resolve-ts.mjs +11 -0
  172. package/dist/resources/extensions/gsd/tests/roadmap-slices.test.ts +41 -0
  173. package/dist/resources/extensions/gsd/tests/run-uat.test.ts +316 -0
  174. package/dist/resources/extensions/gsd/tests/secure-env-collect.test.ts +185 -0
  175. package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +123 -0
  176. package/dist/resources/extensions/gsd/tests/test-helpers.ts +61 -0
  177. package/dist/resources/extensions/gsd/tests/unique-milestone-ids.test.ts +219 -0
  178. package/dist/resources/extensions/gsd/tests/unit-runtime.test.ts +234 -0
  179. package/dist/resources/extensions/gsd/tests/workspace-index.test.ts +75 -0
  180. package/dist/resources/extensions/gsd/tests/worktree-integration.test.ts +234 -0
  181. package/dist/resources/extensions/gsd/tests/worktree-manager.test.ts +141 -0
  182. package/dist/resources/extensions/gsd/tests/worktree.test.ts +597 -0
  183. package/dist/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
  184. package/dist/resources/extensions/gsd/types.ts +296 -0
  185. package/dist/resources/extensions/gsd/unit-runtime.ts +184 -0
  186. package/dist/resources/extensions/gsd/workspace-index.ts +189 -0
  187. package/dist/resources/extensions/gsd/worktree-command.ts +845 -0
  188. package/dist/resources/extensions/gsd/worktree-manager.ts +459 -0
  189. package/dist/resources/extensions/gsd/{worktree.js → worktree.ts} +92 -61
  190. package/dist/resources/extensions/mac-tools/index.ts +852 -0
  191. package/dist/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
  192. package/dist/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
  193. package/dist/resources/extensions/mcporter/index.ts +429 -0
  194. package/dist/resources/extensions/remote-questions/config.ts +81 -0
  195. package/dist/resources/extensions/remote-questions/discord-adapter.ts +128 -0
  196. package/dist/resources/extensions/remote-questions/format.ts +163 -0
  197. package/dist/resources/extensions/remote-questions/manager.ts +192 -0
  198. package/dist/resources/extensions/remote-questions/remote-command.ts +361 -0
  199. package/dist/resources/extensions/remote-questions/slack-adapter.ts +92 -0
  200. package/dist/resources/extensions/remote-questions/status.ts +31 -0
  201. package/dist/resources/extensions/remote-questions/store.ts +77 -0
  202. package/dist/resources/extensions/remote-questions/types.ts +75 -0
  203. package/dist/resources/extensions/search-the-web/cache.ts +78 -0
  204. package/dist/resources/extensions/search-the-web/command-search-provider.ts +99 -0
  205. package/dist/resources/extensions/search-the-web/format.ts +258 -0
  206. package/dist/resources/extensions/search-the-web/http.ts +238 -0
  207. package/dist/resources/extensions/search-the-web/index.ts +65 -0
  208. package/dist/resources/extensions/search-the-web/native-search.ts +165 -0
  209. package/dist/resources/extensions/search-the-web/provider.ts +136 -0
  210. package/dist/resources/extensions/search-the-web/tavily.ts +116 -0
  211. package/dist/resources/extensions/search-the-web/tool-fetch-page.ts +581 -0
  212. package/dist/resources/extensions/search-the-web/tool-llm-context.ts +620 -0
  213. package/dist/resources/extensions/search-the-web/tool-search.ts +635 -0
  214. package/dist/resources/extensions/search-the-web/url-utils.ts +91 -0
  215. package/dist/resources/extensions/shared/bundled-extension-paths.ts +11 -0
  216. package/dist/resources/extensions/shared/confirm-ui.ts +126 -0
  217. package/dist/resources/extensions/shared/interview-ui.ts +613 -0
  218. package/dist/resources/extensions/shared/next-action-ui.ts +197 -0
  219. package/dist/resources/extensions/shared/progress-widget.ts +282 -0
  220. package/dist/resources/extensions/shared/terminal.ts +23 -0
  221. package/dist/resources/extensions/shared/thinking-widget.ts +107 -0
  222. package/dist/resources/extensions/shared/ui.ts +400 -0
  223. package/dist/resources/extensions/shared/wizard-ui.ts +551 -0
  224. package/dist/resources/extensions/slash-commands/audit.ts +88 -0
  225. package/dist/resources/extensions/slash-commands/clear.ts +10 -0
  226. package/dist/resources/extensions/slash-commands/create-extension.ts +297 -0
  227. package/dist/resources/extensions/slash-commands/create-slash-command.ts +234 -0
  228. package/dist/resources/extensions/slash-commands/index.ts +12 -0
  229. package/dist/resources/extensions/subagent/agents.ts +126 -0
  230. package/dist/resources/extensions/subagent/index.ts +1072 -0
  231. package/dist/resources/extensions/subagent/isolation.ts +498 -0
  232. package/dist/resources/extensions/ttsr/index.ts +163 -0
  233. package/dist/resources/extensions/ttsr/rule-loader.ts +121 -0
  234. package/dist/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
  235. package/dist/resources/extensions/ttsr/ttsr-manager.ts +430 -0
  236. package/dist/resources/extensions/universal-config/discovery.ts +78 -0
  237. package/dist/resources/extensions/universal-config/format.ts +160 -0
  238. package/dist/resources/extensions/universal-config/index.ts +118 -0
  239. package/dist/resources/extensions/universal-config/package.json +11 -0
  240. package/dist/resources/extensions/universal-config/scanners.ts +579 -0
  241. package/dist/resources/extensions/universal-config/tests/discovery.test.ts +111 -0
  242. package/dist/resources/extensions/universal-config/tests/format.test.ts +111 -0
  243. package/dist/resources/extensions/universal-config/tests/scanners.test.ts +438 -0
  244. package/dist/resources/extensions/universal-config/tools.ts +60 -0
  245. package/dist/resources/extensions/universal-config/types.ts +116 -0
  246. package/dist/resources/extensions/voice/index.ts +271 -0
  247. package/dist/resources/extensions/voice/speech-recognizer.py +504 -0
  248. package/dist/resources/extensions/voice/speech-recognizer.swift +154 -0
  249. package/dist/resources/skills/debug-like-expert/SKILL.md +231 -0
  250. package/dist/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
  251. package/dist/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
  252. package/dist/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
  253. package/dist/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
  254. package/dist/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
  255. package/dist/resources/skills/frontend-design/SKILL.md +45 -0
  256. package/dist/resources/skills/github-workflows/SKILL.md +92 -0
  257. package/dist/resources/skills/github-workflows/references/gh/SKILL.md +255 -0
  258. package/dist/resources/skills/github-workflows/references/gh/references/issue-stories.md +204 -0
  259. package/dist/resources/skills/github-workflows/references/gh/references/labels.md +170 -0
  260. package/dist/resources/skills/github-workflows/references/gh/references/milestones.md +158 -0
  261. package/dist/resources/skills/github-workflows/references/gh/references/projects-v2.md +177 -0
  262. package/dist/resources/skills/github-workflows/references/gh/scripts/experiment_cleanup.py +191 -0
  263. package/dist/resources/skills/github-workflows/references/gh/scripts/github_project_setup.py +799 -0
  264. package/dist/resources/skills/github-workflows/references/gh/tests/__init__.py +0 -0
  265. package/dist/resources/skills/github-workflows/references/gh/tests/test_github_project_setup.py +608 -0
  266. package/dist/resources/skills/swiftui/SKILL.md +208 -0
  267. package/dist/resources/skills/swiftui/references/animations.md +921 -0
  268. package/dist/resources/skills/swiftui/references/architecture.md +1561 -0
  269. package/dist/resources/skills/swiftui/references/layout-system.md +1186 -0
  270. package/dist/resources/skills/swiftui/references/navigation.md +1492 -0
  271. package/dist/resources/skills/swiftui/references/networking-async.md +214 -0
  272. package/dist/resources/skills/swiftui/references/performance.md +1706 -0
  273. package/dist/resources/skills/swiftui/references/platform-integration.md +204 -0
  274. package/dist/resources/skills/swiftui/references/state-management.md +1443 -0
  275. package/dist/resources/skills/swiftui/references/swiftdata.md +297 -0
  276. package/dist/resources/skills/swiftui/references/testing-debugging.md +247 -0
  277. package/dist/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
  278. package/dist/resources/skills/swiftui/workflows/add-feature.md +191 -0
  279. package/dist/resources/skills/swiftui/workflows/build-new-app.md +311 -0
  280. package/dist/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
  281. package/dist/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
  282. package/dist/resources/skills/swiftui/workflows/ship-app.md +203 -0
  283. package/dist/resources/skills/swiftui/workflows/write-tests.md +235 -0
  284. package/dist/wizard.js +1 -0
  285. package/package.json +8 -6
  286. package/packages/native/dist/fd/index.d.ts +1 -0
  287. package/packages/native/dist/fd/index.js +1 -0
  288. package/packages/native/dist/grep/index.d.ts +2 -1
  289. package/packages/native/dist/grep/index.js +1 -0
  290. package/packages/native/src/__tests__/fd.test.mjs +35 -0
  291. package/packages/native/src/__tests__/grep.test.mjs +26 -9
  292. package/packages/native/src/fd/index.ts +1 -0
  293. package/packages/native/src/grep/index.ts +3 -2
  294. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  295. package/packages/pi-agent-core/dist/agent-loop.js +169 -55
  296. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  297. package/packages/pi-agent-core/dist/agent.d.ts +13 -1
  298. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  299. package/packages/pi-agent-core/dist/agent.js +16 -0
  300. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  301. package/packages/pi-agent-core/dist/types.d.ts +91 -1
  302. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  303. package/packages/pi-agent-core/dist/types.js.map +1 -1
  304. package/packages/pi-agent-core/src/agent-loop.ts +273 -63
  305. package/packages/pi-agent-core/src/agent.ts +24 -0
  306. package/packages/pi-agent-core/src/types.ts +98 -0
  307. package/packages/pi-ai/dist/env-api-keys.js +2 -0
  308. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  309. package/packages/pi-ai/dist/models.generated.d.ts +314 -0
  310. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  311. package/packages/pi-ai/dist/models.generated.js +236 -0
  312. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  313. package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
  314. package/packages/pi-ai/dist/providers/google-gemini-cli.js +6 -0
  315. package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
  316. package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
  317. package/packages/pi-ai/dist/providers/openai-completions.js +2 -0
  318. package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
  319. package/packages/pi-ai/dist/types.d.ts +1 -1
  320. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  321. package/packages/pi-ai/dist/types.js.map +1 -1
  322. package/packages/pi-ai/src/env-api-keys.ts +2 -0
  323. package/packages/pi-ai/src/models.generated.ts +236 -0
  324. package/packages/pi-ai/src/providers/google-gemini-cli.ts +8 -0
  325. package/packages/pi-ai/src/providers/openai-completions.ts +3 -0
  326. package/packages/pi-ai/src/types.ts +2 -1
  327. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  328. package/packages/pi-coding-agent/dist/cli/args.js +1 -0
  329. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  330. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +26 -0
  331. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  332. package/packages/pi-coding-agent/dist/core/agent-session.js +137 -22
  333. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  334. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts +20 -0
  335. package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
  336. package/packages/pi-coding-agent/dist/core/auth-storage.js +43 -0
  337. package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
  338. package/packages/pi-coding-agent/dist/core/fallback-resolver.d.ts +51 -0
  339. package/packages/pi-coding-agent/dist/core/fallback-resolver.d.ts.map +1 -0
  340. package/packages/pi-coding-agent/dist/core/fallback-resolver.js +119 -0
  341. package/packages/pi-coding-agent/dist/core/fallback-resolver.js.map +1 -0
  342. package/packages/pi-coding-agent/dist/core/fallback-resolver.test.d.ts +2 -0
  343. package/packages/pi-coding-agent/dist/core/fallback-resolver.test.d.ts.map +1 -0
  344. package/packages/pi-coding-agent/dist/core/fallback-resolver.test.js +183 -0
  345. package/packages/pi-coding-agent/dist/core/fallback-resolver.test.js.map +1 -0
  346. package/packages/pi-coding-agent/dist/core/index.d.ts +1 -0
  347. package/packages/pi-coding-agent/dist/core/index.d.ts.map +1 -1
  348. package/packages/pi-coding-agent/dist/core/index.js +1 -0
  349. package/packages/pi-coding-agent/dist/core/index.js.map +1 -1
  350. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  351. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  352. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  353. package/packages/pi-coding-agent/dist/core/session-manager.d.ts +12 -1
  354. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  355. package/packages/pi-coding-agent/dist/core/session-manager.js +37 -1
  356. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  357. package/packages/pi-coding-agent/dist/core/session-manager.test.d.ts +2 -0
  358. package/packages/pi-coding-agent/dist/core/session-manager.test.d.ts.map +1 -0
  359. package/packages/pi-coding-agent/dist/core/session-manager.test.js +61 -0
  360. package/packages/pi-coding-agent/dist/core/session-manager.test.js.map +1 -0
  361. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +20 -0
  362. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  363. package/packages/pi-coding-agent/dist/core/settings-manager.js +47 -0
  364. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  365. package/packages/pi-coding-agent/dist/core/tools/find.d.ts.map +1 -1
  366. package/packages/pi-coding-agent/dist/core/tools/find.js +1 -0
  367. package/packages/pi-coding-agent/dist/core/tools/find.js.map +1 -1
  368. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
  369. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +6 -15
  370. package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
  371. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts +18 -1
  372. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
  373. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js +46 -4
  374. package/packages/pi-coding-agent/dist/modes/interactive/components/login-dialog.js.map +1 -1
  375. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  376. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +25 -2
  377. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  378. package/packages/pi-coding-agent/src/cli/args.ts +1 -0
  379. package/packages/pi-coding-agent/src/core/agent-session.ts +167 -22
  380. package/packages/pi-coding-agent/src/core/auth-storage.ts +45 -0
  381. package/packages/pi-coding-agent/src/core/fallback-resolver.test.ts +229 -0
  382. package/packages/pi-coding-agent/src/core/fallback-resolver.ts +165 -0
  383. package/packages/pi-coding-agent/src/core/index.ts +1 -0
  384. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  385. package/packages/pi-coding-agent/src/core/session-manager.test.ts +65 -0
  386. package/packages/pi-coding-agent/src/core/session-manager.ts +51 -1
  387. package/packages/pi-coding-agent/src/core/settings-manager.ts +66 -0
  388. package/packages/pi-coding-agent/src/core/tools/find.ts +1 -0
  389. package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +6 -16
  390. package/packages/pi-coding-agent/src/modes/interactive/components/login-dialog.ts +49 -4
  391. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +28 -2
  392. package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
  393. package/packages/pi-tui/dist/autocomplete.js +3 -4
  394. package/packages/pi-tui/dist/autocomplete.js.map +1 -1
  395. package/packages/pi-tui/dist/components/editor.d.ts +7 -0
  396. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  397. package/packages/pi-tui/dist/components/editor.js +57 -50
  398. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  399. package/packages/pi-tui/src/autocomplete.ts +3 -5
  400. package/packages/pi-tui/src/components/editor.ts +70 -49
  401. package/pkg/dist/core/export-html/ansi-to-html.d.ts +22 -0
  402. package/pkg/dist/core/export-html/ansi-to-html.d.ts.map +1 -0
  403. package/pkg/dist/core/export-html/ansi-to-html.js +249 -0
  404. package/pkg/dist/core/export-html/ansi-to-html.js.map +1 -0
  405. package/pkg/dist/core/export-html/index.d.ts +37 -0
  406. package/pkg/dist/core/export-html/index.d.ts.map +1 -0
  407. package/pkg/dist/core/export-html/index.js +223 -0
  408. package/pkg/dist/core/export-html/index.js.map +1 -0
  409. package/pkg/dist/core/export-html/template.css +971 -0
  410. package/pkg/dist/core/export-html/template.html +54 -0
  411. package/pkg/dist/core/export-html/template.js +1583 -0
  412. package/pkg/dist/core/export-html/tool-renderer.d.ts +38 -0
  413. package/pkg/dist/core/export-html/tool-renderer.d.ts.map +1 -0
  414. package/pkg/dist/core/export-html/tool-renderer.js +70 -0
  415. package/pkg/dist/core/export-html/tool-renderer.js.map +1 -0
  416. package/pkg/dist/core/export-html/vendor/highlight.min.js +1213 -0
  417. package/pkg/dist/core/export-html/vendor/marked.min.js +6 -0
  418. package/scripts/postinstall.js +30 -17
  419. package/src/resources/extensions/bg-shell/index.ts +51 -7
  420. package/src/resources/extensions/gsd/activity-log.ts +61 -14
  421. package/src/resources/extensions/gsd/auto.ts +288 -19
  422. package/src/resources/extensions/gsd/commands.ts +9 -3
  423. package/src/resources/extensions/gsd/dashboard-overlay.ts +55 -12
  424. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -0
  425. package/src/resources/extensions/gsd/doctor.ts +60 -3
  426. package/src/resources/extensions/gsd/files.ts +45 -1
  427. package/src/resources/extensions/gsd/git-service.ts +46 -41
  428. package/src/resources/extensions/gsd/guided-flow.ts +81 -9
  429. package/src/resources/extensions/gsd/index.ts +62 -1
  430. package/src/resources/extensions/gsd/native-git-bridge.ts +181 -0
  431. package/src/resources/extensions/gsd/paths.ts +35 -4
  432. package/src/resources/extensions/gsd/post-unit-hooks.ts +449 -0
  433. package/src/resources/extensions/gsd/preferences.ts +232 -1
  434. package/src/resources/extensions/gsd/prompt-loader.ts +45 -3
  435. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  436. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -3
  437. package/src/resources/extensions/gsd/prompts/discuss.md +10 -8
  438. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
  439. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
  440. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
  441. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  442. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
  443. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
  444. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
  445. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
  446. package/src/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
  447. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -3
  448. package/src/resources/extensions/gsd/prompts/queue.md +3 -1
  449. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  450. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  451. package/src/resources/extensions/gsd/state.ts +130 -18
  452. package/src/resources/extensions/gsd/templates/context.md +1 -1
  453. package/src/resources/extensions/gsd/templates/state.md +3 -3
  454. package/src/resources/extensions/gsd/tests/activity-log-save.test.ts +127 -0
  455. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +5 -1
  456. package/src/resources/extensions/gsd/tests/doctor.test.ts +115 -1
  457. package/src/resources/extensions/gsd/tests/draft-promotion.test.ts +6 -2
  458. package/src/resources/extensions/gsd/tests/git-service.test.ts +35 -35
  459. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +8 -8
  460. package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +4 -1
  461. package/src/resources/extensions/gsd/tests/network-error-fallback.test.ts +54 -0
  462. package/src/resources/extensions/gsd/tests/orphaned-branch.test.ts +5 -5
  463. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
  464. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
  465. package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
  466. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +6 -0
  467. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +4 -4
  468. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +4 -4
  469. package/src/resources/extensions/gsd/tests/worktree.test.ts +19 -19
  470. package/src/resources/extensions/gsd/types.ts +109 -0
  471. package/src/resources/extensions/gsd/worktree-manager.ts +72 -13
  472. package/src/resources/extensions/gsd/worktree.ts +5 -4
  473. package/src/resources/extensions/search-the-web/command-search-provider.ts +8 -4
  474. package/src/resources/extensions/search-the-web/native-search.ts +32 -23
  475. package/src/resources/extensions/search-the-web/provider.ts +19 -2
  476. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
  477. package/src/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
  478. package/src/resources/extensions/search-the-web/tool-search.ts +62 -3
  479. package/src/resources/extensions/shared/bundled-extension-paths.ts +11 -0
  480. package/src/resources/extensions/subagent/index.ts +2 -1
  481. package/src/resources/skills/github-workflows/SKILL.md +5 -0
  482. package/dist/resources/extensions/gsd/git-service.d.ts +0 -225
  483. package/dist/resources/extensions/gsd/git-service.js +0 -756
  484. package/dist/resources/extensions/gsd/preferences.d.ts +0 -122
  485. package/dist/resources/extensions/gsd/preferences.js +0 -616
  486. package/dist/resources/extensions/gsd/worktree.d.ts +0 -106
  487. package/dist/resources/extensions/remote-questions/config.d.ts +0 -14
  488. package/dist/resources/extensions/remote-questions/config.js +0 -68
  489. package/dist/resources/extensions/remote-questions/discord-adapter.d.ts +0 -17
  490. package/dist/resources/extensions/remote-questions/discord-adapter.js +0 -118
  491. package/dist/resources/extensions/remote-questions/format.d.ts +0 -38
  492. package/dist/resources/extensions/remote-questions/format.js +0 -119
  493. package/dist/resources/extensions/remote-questions/manager.d.ts +0 -23
  494. package/dist/resources/extensions/remote-questions/manager.js +0 -161
  495. package/dist/resources/extensions/remote-questions/remote-command.d.ts +0 -6
  496. package/dist/resources/extensions/remote-questions/remote-command.js +0 -358
  497. package/dist/resources/extensions/remote-questions/slack-adapter.d.ts +0 -15
  498. package/dist/resources/extensions/remote-questions/slack-adapter.js +0 -82
  499. package/dist/resources/extensions/remote-questions/status.d.ts +0 -9
  500. package/dist/resources/extensions/remote-questions/status.js +0 -25
  501. package/dist/resources/extensions/remote-questions/store.d.ts +0 -11
  502. package/dist/resources/extensions/remote-questions/store.js +0 -66
  503. package/dist/resources/extensions/remote-questions/types.d.ts +0 -68
  504. package/dist/resources/extensions/remote-questions/types.js +0 -4
@@ -0,0 +1,988 @@
1
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { isAbsolute, join } from "node:path";
4
+ import { getAgentDir } from "@gsd/pi-coding-agent";
5
+ import type { GitPreferences } from "./git-service.js";
6
+ import type { PostUnitHookConfig, PreDispatchHookConfig } from "./types.js";
7
+ import { VALID_BRANCH_NAME } from "./git-service.js";
8
+
9
+ const GLOBAL_PREFERENCES_PATH = join(homedir(), ".gsd", "preferences.md");
10
+ const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md");
11
+ const PROJECT_PREFERENCES_PATH = join(process.cwd(), ".gsd", "preferences.md");
12
+ // Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
13
+ // Check uppercase as a fallback so those files aren't silently ignored.
14
+ const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(homedir(), ".gsd", "PREFERENCES.md");
15
+ const PROJECT_PREFERENCES_PATH_UPPERCASE = join(process.cwd(), ".gsd", "PREFERENCES.md");
16
+ const SKILL_ACTIONS = new Set(["use", "prefer", "avoid"]);
17
+
18
+ export interface GSDSkillRule {
19
+ when: string;
20
+ use?: string[];
21
+ prefer?: string[];
22
+ avoid?: string[];
23
+ }
24
+
25
+ /**
26
+ * Model configuration for a single phase.
27
+ * Supports primary model with optional fallbacks for resilience.
28
+ */
29
+ export interface GSDPhaseModelConfig {
30
+ /** Primary model ID (e.g., "claude-opus-4-6") */
31
+ model: string;
32
+ /** Provider name to disambiguate when the same model ID exists across providers (e.g., "bedrock", "anthropic") */
33
+ provider?: string;
34
+ /** Fallback models to try in order if primary fails (e.g., rate limits, credits exhausted) */
35
+ fallbacks?: string[];
36
+ }
37
+
38
+ /**
39
+ * Legacy model config — simple string per phase.
40
+ * Kept for backward compatibility; will be migrated to GSDModelConfigV2 on load.
41
+ */
42
+ export interface GSDModelConfig {
43
+ research?: string;
44
+ planning?: string;
45
+ execution?: string;
46
+ completion?: string;
47
+ }
48
+
49
+ /**
50
+ * Extended model config with per-phase fallback support.
51
+ * Each phase can specify a primary model and ordered fallbacks.
52
+ */
53
+ export interface GSDModelConfigV2 {
54
+ research?: string | GSDPhaseModelConfig;
55
+ planning?: string | GSDPhaseModelConfig;
56
+ execution?: string | GSDPhaseModelConfig;
57
+ completion?: string | GSDPhaseModelConfig;
58
+ }
59
+
60
+ /** Normalized model selection with resolved fallbacks */
61
+ export interface ResolvedModelConfig {
62
+ primary: string;
63
+ fallbacks: string[];
64
+ }
65
+
66
+ export type SkillDiscoveryMode = "auto" | "suggest" | "off";
67
+
68
+ export interface AutoSupervisorConfig {
69
+ model?: string;
70
+ soft_timeout_minutes?: number;
71
+ idle_timeout_minutes?: number;
72
+ hard_timeout_minutes?: number;
73
+ }
74
+
75
+ export interface RemoteQuestionsConfig {
76
+ channel: "slack" | "discord";
77
+ channel_id: string | number;
78
+ timeout_minutes?: number; // clamped to 1-30
79
+ poll_interval_seconds?: number; // clamped to 2-30
80
+ }
81
+
82
+ export interface GSDPreferences {
83
+ version?: number;
84
+ always_use_skills?: string[];
85
+ prefer_skills?: string[];
86
+ avoid_skills?: string[];
87
+ skill_rules?: GSDSkillRule[];
88
+ custom_instructions?: string[];
89
+ models?: GSDModelConfig;
90
+ skill_discovery?: SkillDiscoveryMode;
91
+ auto_supervisor?: AutoSupervisorConfig;
92
+ uat_dispatch?: boolean;
93
+ unique_milestone_ids?: boolean;
94
+ budget_ceiling?: number;
95
+ remote_questions?: RemoteQuestionsConfig;
96
+ git?: GitPreferences;
97
+ post_unit_hooks?: PostUnitHookConfig[];
98
+ pre_dispatch_hooks?: PreDispatchHookConfig[];
99
+ }
100
+
101
+ export interface LoadedGSDPreferences {
102
+ path: string;
103
+ scope: "global" | "project";
104
+ preferences: GSDPreferences;
105
+ }
106
+
107
+ export function getGlobalGSDPreferencesPath(): string {
108
+ return GLOBAL_PREFERENCES_PATH;
109
+ }
110
+
111
+ export function getLegacyGlobalGSDPreferencesPath(): string {
112
+ return LEGACY_GLOBAL_PREFERENCES_PATH;
113
+ }
114
+
115
+ export function getProjectGSDPreferencesPath(): string {
116
+ return PROJECT_PREFERENCES_PATH;
117
+ }
118
+
119
+ export function loadGlobalGSDPreferences(): LoadedGSDPreferences | null {
120
+ return loadPreferencesFile(GLOBAL_PREFERENCES_PATH, "global")
121
+ ?? loadPreferencesFile(GLOBAL_PREFERENCES_PATH_UPPERCASE, "global")
122
+ ?? loadPreferencesFile(LEGACY_GLOBAL_PREFERENCES_PATH, "global");
123
+ }
124
+
125
+ export function loadProjectGSDPreferences(): LoadedGSDPreferences | null {
126
+ return loadPreferencesFile(PROJECT_PREFERENCES_PATH, "project")
127
+ ?? loadPreferencesFile(PROJECT_PREFERENCES_PATH_UPPERCASE, "project");
128
+ }
129
+
130
+ export function loadEffectiveGSDPreferences(): LoadedGSDPreferences | null {
131
+ const globalPreferences = loadGlobalGSDPreferences();
132
+ const projectPreferences = loadProjectGSDPreferences();
133
+
134
+ if (!globalPreferences && !projectPreferences) return null;
135
+ if (!globalPreferences) return projectPreferences;
136
+ if (!projectPreferences) return globalPreferences;
137
+
138
+ return {
139
+ path: projectPreferences.path,
140
+ scope: "project",
141
+ preferences: mergePreferences(globalPreferences.preferences, projectPreferences.preferences),
142
+ };
143
+ }
144
+
145
+ // ─── Skill Reference Resolution ───────────────────────────────────────────────
146
+
147
+ export interface SkillResolution {
148
+ /** The original reference from preferences (bare name or path). */
149
+ original: string;
150
+ /** The resolved absolute path to the SKILL.md file, or null if unresolved. */
151
+ resolvedPath: string | null;
152
+ /** How it was resolved. */
153
+ method: "absolute-path" | "absolute-dir" | "user-skill" | "project-skill" | "unresolved";
154
+ }
155
+
156
+ export interface SkillResolutionReport {
157
+ /** All resolution results, keyed by original reference. */
158
+ resolutions: Map<string, SkillResolution>;
159
+ /** References that could not be resolved. */
160
+ warnings: string[];
161
+ }
162
+
163
+ /**
164
+ * Known skill directories, in priority order.
165
+ * User skills (~/.gsd/agent/skills/) take precedence over project skills.
166
+ */
167
+ function getSkillSearchDirs(cwd: string): Array<{ dir: string; method: SkillResolution["method"] }> {
168
+ return [
169
+ { dir: join(getAgentDir(), "skills"), method: "user-skill" },
170
+ { dir: join(cwd, ".pi", "agent", "skills"), method: "project-skill" },
171
+ ];
172
+ }
173
+
174
+ /**
175
+ * Resolve a single skill reference to an absolute path.
176
+ *
177
+ * Resolution order:
178
+ * 1. Absolute path to a file → check existsSync
179
+ * 2. Absolute path to a directory → check for SKILL.md inside
180
+ * 3. Bare name → scan known skill directories for <name>/SKILL.md
181
+ */
182
+ function resolveSkillReference(ref: string, cwd: string): SkillResolution {
183
+ const trimmed = ref.trim();
184
+
185
+ // Expand tilde
186
+ const expanded = trimmed.startsWith("~/")
187
+ ? join(homedir(), trimmed.slice(2))
188
+ : trimmed;
189
+
190
+ // Absolute path
191
+ if (isAbsolute(expanded)) {
192
+ // Direct file reference
193
+ if (existsSync(expanded)) {
194
+ // Check if it's a directory — look for SKILL.md inside
195
+ try {
196
+ const stat = statSync(expanded);
197
+ if (stat.isDirectory()) {
198
+ const skillFile = join(expanded, "SKILL.md");
199
+ if (existsSync(skillFile)) {
200
+ return { original: ref, resolvedPath: skillFile, method: "absolute-dir" };
201
+ }
202
+ return { original: ref, resolvedPath: null, method: "unresolved" };
203
+ }
204
+ } catch { /* fall through */ }
205
+ return { original: ref, resolvedPath: expanded, method: "absolute-path" };
206
+ }
207
+ // Maybe it's a directory path without SKILL.md suffix
208
+ const withSkillMd = join(expanded, "SKILL.md");
209
+ if (existsSync(withSkillMd)) {
210
+ return { original: ref, resolvedPath: withSkillMd, method: "absolute-dir" };
211
+ }
212
+ return { original: ref, resolvedPath: null, method: "unresolved" };
213
+ }
214
+
215
+ // Bare name — scan known skill directories
216
+ for (const { dir, method } of getSkillSearchDirs(cwd)) {
217
+ if (!existsSync(dir)) continue;
218
+ try {
219
+ const entries = readdirSync(dir, { withFileTypes: true });
220
+ for (const entry of entries) {
221
+ if (!entry.isDirectory()) continue;
222
+ if (entry.name === expanded) {
223
+ const skillFile = join(dir, entry.name, "SKILL.md");
224
+ if (existsSync(skillFile)) {
225
+ return { original: ref, resolvedPath: skillFile, method };
226
+ }
227
+ }
228
+ }
229
+ } catch { /* directory not readable — skip */ }
230
+ }
231
+
232
+ return { original: ref, resolvedPath: null, method: "unresolved" };
233
+ }
234
+
235
+ /**
236
+ * Resolve all skill references in a preferences object.
237
+ * Caches resolution per reference string to avoid redundant filesystem scans.
238
+ */
239
+ export function resolveAllSkillReferences(preferences: GSDPreferences, cwd: string): SkillResolutionReport {
240
+ const validated = validatePreferences(preferences).preferences;
241
+ preferences = validated;
242
+
243
+ const resolutions = new Map<string, SkillResolution>();
244
+ const warnings: string[] = [];
245
+
246
+ function resolve(ref: string): SkillResolution {
247
+ const existing = resolutions.get(ref);
248
+ if (existing) return existing;
249
+ const result = resolveSkillReference(ref, cwd);
250
+ resolutions.set(ref, result);
251
+ if (result.method === "unresolved") {
252
+ warnings.push(ref);
253
+ }
254
+ return result;
255
+ }
256
+
257
+ // Resolve all skill lists
258
+ for (const skill of preferences.always_use_skills ?? []) resolve(skill);
259
+ for (const skill of preferences.prefer_skills ?? []) resolve(skill);
260
+ for (const skill of preferences.avoid_skills ?? []) resolve(skill);
261
+
262
+ // Resolve skill rules
263
+ for (const rule of preferences.skill_rules ?? []) {
264
+ for (const skill of rule.use ?? []) resolve(skill);
265
+ for (const skill of rule.prefer ?? []) resolve(skill);
266
+ for (const skill of rule.avoid ?? []) resolve(skill);
267
+ }
268
+
269
+ return { resolutions, warnings };
270
+ }
271
+
272
+ /**
273
+ * Format a skill reference for the system prompt.
274
+ * If resolved, shows the path so the agent knows exactly where to read.
275
+ * If unresolved, marks it clearly.
276
+ */
277
+ function formatSkillRef(ref: string, resolutions: Map<string, SkillResolution>): string {
278
+ const resolution = resolutions.get(ref);
279
+ if (!resolution || resolution.method === "unresolved") {
280
+ return `${ref} (⚠ not found — check skill name or path)`;
281
+ }
282
+ // For absolute paths where SKILL.md is just appended, don't clutter the output
283
+ if (resolution.method === "absolute-path" || resolution.method === "absolute-dir") {
284
+ return ref;
285
+ }
286
+ // For bare names resolved from skill directories, show the resolved path
287
+ return `${ref} → \`${resolution.resolvedPath}\``;
288
+ }
289
+
290
+ // ─── System Prompt Rendering ──────────────────────────────────────────────────
291
+
292
+ export function renderPreferencesForSystemPrompt(preferences: GSDPreferences, resolutions?: Map<string, SkillResolution>): string {
293
+ const validated = validatePreferences(preferences);
294
+ const lines: string[] = ["## GSD Skill Preferences"];
295
+
296
+ if (validated.errors.length > 0) {
297
+ lines.push("- Validation: some preference values were ignored because they were invalid.");
298
+ }
299
+
300
+ preferences = validated.preferences;
301
+
302
+ lines.push(
303
+ "- Treat these as explicit skill-selection policy for GSD work.",
304
+ "- If a listed skill exists and is relevant, load and follow it instead of treating it as a vague suggestion.",
305
+ "- Current user instructions still override these defaults.",
306
+ );
307
+
308
+ const fmt = (ref: string) => resolutions ? formatSkillRef(ref, resolutions) : ref;
309
+
310
+ if (preferences.always_use_skills && preferences.always_use_skills.length > 0) {
311
+ lines.push("- Always use these skills when relevant:");
312
+ for (const skill of preferences.always_use_skills) {
313
+ lines.push(` - ${fmt(skill)}`);
314
+ }
315
+ }
316
+
317
+ if (preferences.prefer_skills && preferences.prefer_skills.length > 0) {
318
+ lines.push("- Prefer these skills when relevant:");
319
+ for (const skill of preferences.prefer_skills) {
320
+ lines.push(` - ${fmt(skill)}`);
321
+ }
322
+ }
323
+
324
+ if (preferences.avoid_skills && preferences.avoid_skills.length > 0) {
325
+ lines.push("- Avoid these skills unless clearly needed:");
326
+ for (const skill of preferences.avoid_skills) {
327
+ lines.push(` - ${fmt(skill)}`);
328
+ }
329
+ }
330
+
331
+ if (preferences.skill_rules && preferences.skill_rules.length > 0) {
332
+ lines.push("- Situational rules:");
333
+ for (const rule of preferences.skill_rules) {
334
+ lines.push(` - When ${rule.when}:`);
335
+ if (rule.use && rule.use.length > 0) {
336
+ lines.push(` - use: ${rule.use.map(fmt).join(", ")}`);
337
+ }
338
+ if (rule.prefer && rule.prefer.length > 0) {
339
+ lines.push(` - prefer: ${rule.prefer.map(fmt).join(", ")}`);
340
+ }
341
+ if (rule.avoid && rule.avoid.length > 0) {
342
+ lines.push(` - avoid: ${rule.avoid.map(fmt).join(", ")}`);
343
+ }
344
+ }
345
+ }
346
+
347
+ if (preferences.custom_instructions && preferences.custom_instructions.length > 0) {
348
+ lines.push("- Additional instructions:");
349
+ for (const instruction of preferences.custom_instructions) {
350
+ lines.push(` - ${instruction}`);
351
+ }
352
+ }
353
+
354
+ return lines.join("\n");
355
+ }
356
+
357
+ function loadPreferencesFile(path: string, scope: "global" | "project"): LoadedGSDPreferences | null {
358
+ if (!existsSync(path)) return null;
359
+
360
+ const raw = readFileSync(path, "utf-8");
361
+ const preferences = parsePreferencesMarkdown(raw);
362
+ if (!preferences) return null;
363
+
364
+ return {
365
+ path,
366
+ scope,
367
+ preferences,
368
+ };
369
+ }
370
+
371
+ function parsePreferencesMarkdown(content: string): GSDPreferences | null {
372
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
373
+ if (!match) return null;
374
+ return parseFrontmatterBlock(match[1]);
375
+ }
376
+
377
+ function parseFrontmatterBlock(frontmatter: string): GSDPreferences {
378
+ const root: Record<string, unknown> = {};
379
+ const stack: Array<{ indent: number; value: Record<string, unknown> }> = [{ indent: -1, value: root }];
380
+
381
+ const lines = frontmatter.split(/\r?\n/);
382
+ for (let i = 0; i < lines.length; i++) {
383
+ const line = lines[i];
384
+ if (!line.trim()) continue;
385
+
386
+ const indent = line.match(/^\s*/)?.[0].length ?? 0;
387
+ const trimmed = line.trim();
388
+
389
+ while (stack.length > 1 && indent <= stack[stack.length - 1].indent) {
390
+ stack.pop();
391
+ }
392
+
393
+ const current = stack[stack.length - 1].value;
394
+ const keyMatch = trimmed.match(/^([A-Za-z0-9_]+):(.*)$/);
395
+ if (!keyMatch) continue;
396
+
397
+ const [, key, remainder] = keyMatch;
398
+ const valuePart = remainder.trim();
399
+
400
+ if (valuePart === "") {
401
+ const nextLine = lines[i + 1] ?? "";
402
+ const nextTrimmed = nextLine.trim();
403
+ if (nextTrimmed.startsWith("- ")) {
404
+ const items: unknown[] = [];
405
+ let j = i + 1;
406
+ while (j < lines.length) {
407
+ const candidate = lines[j];
408
+ const candidateIndent = candidate.match(/^\s*/)?.[0].length ?? 0;
409
+ const candidateTrimmed = candidate.trim();
410
+ if (!candidateTrimmed) {
411
+ j++;
412
+ continue;
413
+ }
414
+ if (candidateIndent <= indent || !candidateTrimmed.startsWith("- ")) break;
415
+
416
+ const itemText = candidateTrimmed.slice(2).trim();
417
+ const nextCandidate = lines[j + 1] ?? "";
418
+ const nextCandidateIndent = nextCandidate.match(/^\s*/)?.[0].length ?? 0;
419
+ const nextCandidateTrimmed = nextCandidate.trim();
420
+
421
+ if (itemText.includes(":") || (nextCandidateTrimmed && nextCandidateIndent > candidateIndent)) {
422
+ const obj: Record<string, unknown> = {};
423
+ const firstMatch = itemText.match(/^([A-Za-z0-9_]+):(.*)$/);
424
+ if (firstMatch) {
425
+ obj[firstMatch[1]] = parseScalar(firstMatch[2].trim());
426
+ }
427
+ j++;
428
+ while (j < lines.length) {
429
+ const nested = lines[j];
430
+ const nestedIndent = nested.match(/^\s*/)?.[0].length ?? 0;
431
+ const nestedTrimmed = nested.trim();
432
+ if (!nestedTrimmed) {
433
+ j++;
434
+ continue;
435
+ }
436
+ if (nestedIndent <= candidateIndent) break;
437
+ const nestedMatch = nestedTrimmed.match(/^([A-Za-z0-9_]+):(.*)$/);
438
+ if (nestedMatch) {
439
+ const nestedValue = nestedMatch[2].trim();
440
+ if (nestedValue === "") {
441
+ const nestedItems: string[] = [];
442
+ j++;
443
+ while (j < lines.length) {
444
+ const nestedArrayLine = lines[j];
445
+ const nestedArrayIndent = nestedArrayLine.match(/^\s*/)?.[0].length ?? 0;
446
+ const nestedArrayTrimmed = nestedArrayLine.trim();
447
+ if (!nestedArrayTrimmed) {
448
+ j++;
449
+ continue;
450
+ }
451
+ if (nestedArrayIndent <= nestedIndent || !nestedArrayTrimmed.startsWith("- ")) break;
452
+ nestedItems.push(String(parseScalar(nestedArrayTrimmed.slice(2).trim())));
453
+ j++;
454
+ }
455
+ obj[nestedMatch[1]] = nestedItems;
456
+ continue;
457
+ }
458
+ obj[nestedMatch[1]] = parseScalar(nestedValue);
459
+ }
460
+ j++;
461
+ }
462
+ items.push(obj);
463
+ continue;
464
+ }
465
+
466
+ items.push(parseScalar(itemText));
467
+ j++;
468
+ }
469
+ current[key] = items;
470
+ i = j - 1;
471
+ } else {
472
+ const obj: Record<string, unknown> = {};
473
+ current[key] = obj;
474
+ stack.push({ indent, value: obj });
475
+ }
476
+ continue;
477
+ }
478
+
479
+ current[key] = parseScalar(valuePart);
480
+ }
481
+
482
+ return root as GSDPreferences;
483
+ }
484
+
485
+ function parseScalar(value: string): string | number | boolean {
486
+ if (value === "true") return true;
487
+ if (value === "false") return false;
488
+ if (/^-?\d+$/.test(value)) {
489
+ const n = Number(value);
490
+ // Keep large integers (e.g. Discord channel IDs) as strings to avoid precision loss
491
+ if (Number.isSafeInteger(n)) return n;
492
+ return value;
493
+ }
494
+ return value.replace(/^['\"]|['\"]$/g, "");
495
+ }
496
+
497
+ /**
498
+ * Resolve the skill discovery mode from effective preferences.
499
+ * Defaults to "suggest" — skills are identified during research but not installed automatically.
500
+ */
501
+ export function resolveSkillDiscoveryMode(): SkillDiscoveryMode {
502
+ const prefs = loadEffectiveGSDPreferences();
503
+ return prefs?.preferences.skill_discovery ?? "suggest";
504
+ }
505
+
506
+ /**
507
+ * Resolve which model ID to use for a given auto-mode unit type.
508
+ * Returns undefined if no model preference is set for this unit type.
509
+ */
510
+ export function resolveModelForUnit(unitType: string): string | undefined {
511
+ const resolved = resolveModelWithFallbacksForUnit(unitType);
512
+ return resolved?.primary;
513
+ }
514
+
515
+ /**
516
+ * Resolve model and fallbacks for a given auto-mode unit type.
517
+ * Returns the primary model and ordered fallbacks, or undefined if not configured.
518
+ *
519
+ * Supports both legacy string format and extended object format:
520
+ * - Legacy: `planning: claude-opus-4-6`
521
+ * - Extended: `planning: { model: claude-opus-4-6, fallbacks: [glm-5, minimax-m2.5] }`
522
+ */
523
+ /**
524
+ * Determines the next fallback model to try when the current model fails.
525
+ * If the current model is not in the configured list, returns the primary model.
526
+ * If the current model is the last in the list, returns undefined (exhausted).
527
+ */
528
+ export function getNextFallbackModel(
529
+ currentModelId: string | undefined,
530
+ modelConfig: ResolvedModelConfig,
531
+ ): string | undefined {
532
+ const modelsToTry = [modelConfig.primary, ...modelConfig.fallbacks];
533
+
534
+ if (!currentModelId) {
535
+ return modelsToTry[0];
536
+ }
537
+
538
+ let foundCurrent = false;
539
+ for (let i = 0; i < modelsToTry.length; i++) {
540
+ const mId = modelsToTry[i];
541
+ // Check for exact match or provider/model suffix match
542
+ if (mId === currentModelId || (mId.includes("/") && mId.endsWith(`/${currentModelId}`))) {
543
+ foundCurrent = true;
544
+ return modelsToTry[i + 1]; // Return the next one, or undefined if at the end
545
+ }
546
+ }
547
+
548
+ // If the current model wasn't in our preference list, default to starting the sequence
549
+ if (!foundCurrent) {
550
+ return modelsToTry[0];
551
+ }
552
+ }
553
+
554
+ export function resolveModelWithFallbacksForUnit(unitType: string): ResolvedModelConfig | undefined {
555
+ const prefs = loadEffectiveGSDPreferences();
556
+ if (!prefs?.preferences.models) return undefined;
557
+ const m = prefs.preferences.models as GSDModelConfigV2;
558
+
559
+ let phaseConfig: string | GSDPhaseModelConfig | undefined;
560
+ switch (unitType) {
561
+ case "research-milestone":
562
+ case "research-slice":
563
+ phaseConfig = m.research;
564
+ break;
565
+ case "plan-milestone":
566
+ case "plan-slice":
567
+ case "replan-slice":
568
+ phaseConfig = m.planning;
569
+ break;
570
+ case "execute-task":
571
+ phaseConfig = m.execution;
572
+ break;
573
+ case "complete-slice":
574
+ case "run-uat":
575
+ phaseConfig = m.completion;
576
+ break;
577
+ default:
578
+ return undefined;
579
+ }
580
+
581
+ if (!phaseConfig) return undefined;
582
+
583
+ // Normalize: string -> { model, fallbacks: [] }
584
+ if (typeof phaseConfig === "string") {
585
+ return { primary: phaseConfig, fallbacks: [] };
586
+ }
587
+
588
+ // When provider is explicitly set, prepend it to the model ID so the
589
+ // resolution code in auto.ts can do an explicit provider match.
590
+ const primary = phaseConfig.provider && !phaseConfig.model.includes("/")
591
+ ? `${phaseConfig.provider}/${phaseConfig.model}`
592
+ : phaseConfig.model;
593
+
594
+ return {
595
+ primary,
596
+ fallbacks: phaseConfig.fallbacks ?? [],
597
+ };
598
+ }
599
+
600
+ export function resolveAutoSupervisorConfig(): AutoSupervisorConfig {
601
+ const prefs = loadEffectiveGSDPreferences();
602
+ const configured = prefs?.preferences.auto_supervisor ?? {};
603
+
604
+ return {
605
+ soft_timeout_minutes: configured.soft_timeout_minutes ?? 20,
606
+ idle_timeout_minutes: configured.idle_timeout_minutes ?? 10,
607
+ hard_timeout_minutes: configured.hard_timeout_minutes ?? 30,
608
+ ...(configured.model ? { model: configured.model } : {}),
609
+ };
610
+ }
611
+
612
+ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPreferences {
613
+ return {
614
+ version: override.version ?? base.version,
615
+ always_use_skills: mergeStringLists(base.always_use_skills, override.always_use_skills),
616
+ prefer_skills: mergeStringLists(base.prefer_skills, override.prefer_skills),
617
+ avoid_skills: mergeStringLists(base.avoid_skills, override.avoid_skills),
618
+ skill_rules: [...(base.skill_rules ?? []), ...(override.skill_rules ?? [])],
619
+ custom_instructions: mergeStringLists(base.custom_instructions, override.custom_instructions),
620
+ models: { ...(base.models ?? {}), ...(override.models ?? {}) },
621
+ skill_discovery: override.skill_discovery ?? base.skill_discovery,
622
+ auto_supervisor: { ...(base.auto_supervisor ?? {}), ...(override.auto_supervisor ?? {}) },
623
+ uat_dispatch: override.uat_dispatch ?? base.uat_dispatch,
624
+ unique_milestone_ids: override.unique_milestone_ids ?? base.unique_milestone_ids,
625
+ budget_ceiling: override.budget_ceiling ?? base.budget_ceiling,
626
+ remote_questions: override.remote_questions
627
+ ? { ...(base.remote_questions ?? {}), ...override.remote_questions }
628
+ : base.remote_questions,
629
+ git: (base.git || override.git)
630
+ ? { ...(base.git ?? {}), ...(override.git ?? {}) }
631
+ : undefined,
632
+ post_unit_hooks: mergePostUnitHooks(base.post_unit_hooks, override.post_unit_hooks),
633
+ pre_dispatch_hooks: mergePreDispatchHooks(base.pre_dispatch_hooks, override.pre_dispatch_hooks),
634
+ };
635
+ }
636
+
637
+ function validatePreferences(preferences: GSDPreferences): {
638
+ preferences: GSDPreferences;
639
+ errors: string[];
640
+ } {
641
+ const errors: string[] = [];
642
+ const validated: GSDPreferences = {};
643
+
644
+ if (preferences.version !== undefined) {
645
+ if (preferences.version === 1) {
646
+ validated.version = 1;
647
+ } else {
648
+ errors.push(`unsupported version ${preferences.version}`);
649
+ }
650
+ }
651
+
652
+ const validDiscoveryModes = new Set(["auto", "suggest", "off"]);
653
+ if (preferences.skill_discovery) {
654
+ if (validDiscoveryModes.has(preferences.skill_discovery)) {
655
+ validated.skill_discovery = preferences.skill_discovery;
656
+ } else {
657
+ errors.push(`invalid skill_discovery value: ${preferences.skill_discovery}`);
658
+ }
659
+ }
660
+
661
+ validated.always_use_skills = normalizeStringList(preferences.always_use_skills);
662
+ validated.prefer_skills = normalizeStringList(preferences.prefer_skills);
663
+ validated.avoid_skills = normalizeStringList(preferences.avoid_skills);
664
+ validated.custom_instructions = normalizeStringList(preferences.custom_instructions);
665
+
666
+ if (preferences.skill_rules) {
667
+ const validRules: GSDSkillRule[] = [];
668
+ for (const rule of preferences.skill_rules) {
669
+ if (!rule || typeof rule !== "object") {
670
+ errors.push("invalid skill_rules entry");
671
+ continue;
672
+ }
673
+ const when = typeof rule.when === "string" ? rule.when.trim() : "";
674
+ if (!when) {
675
+ errors.push("skill_rules entry missing when");
676
+ continue;
677
+ }
678
+ const validatedRule: GSDSkillRule = { when };
679
+ for (const action of SKILL_ACTIONS) {
680
+ const values = normalizeStringList((rule as unknown as Record<string, unknown>)[action]);
681
+ if (values.length > 0) {
682
+ validatedRule[action as keyof GSDSkillRule] = values as never;
683
+ }
684
+ }
685
+ if (!validatedRule.use && !validatedRule.prefer && !validatedRule.avoid) {
686
+ errors.push(`skill rule has no actions: ${when}`);
687
+ continue;
688
+ }
689
+ validRules.push(validatedRule);
690
+ }
691
+ if (validRules.length > 0) {
692
+ validated.skill_rules = validRules;
693
+ }
694
+ }
695
+
696
+ for (const key of ["always_use_skills", "prefer_skills", "avoid_skills", "custom_instructions"] as const) {
697
+ if (validated[key] && validated[key]!.length === 0) {
698
+ delete validated[key];
699
+ }
700
+ }
701
+
702
+ if (preferences.uat_dispatch !== undefined) {
703
+ validated.uat_dispatch = !!preferences.uat_dispatch;
704
+ }
705
+
706
+ if (preferences.unique_milestone_ids !== undefined) {
707
+ validated.unique_milestone_ids = !!preferences.unique_milestone_ids;
708
+ }
709
+
710
+ if (preferences.budget_ceiling !== undefined) {
711
+ const raw = preferences.budget_ceiling;
712
+ if (typeof raw === "number" && Number.isFinite(raw)) {
713
+ validated.budget_ceiling = raw;
714
+ } else if (typeof raw === "string" && Number.isFinite(Number(raw))) {
715
+ validated.budget_ceiling = Number(raw);
716
+ } else {
717
+ errors.push("budget_ceiling must be a finite number");
718
+ }
719
+ }
720
+
721
+ // ─── Post-Unit Hooks ─────────────────────────────────────────────────
722
+ if (preferences.post_unit_hooks && Array.isArray(preferences.post_unit_hooks)) {
723
+ const validHooks: PostUnitHookConfig[] = [];
724
+ const seenNames = new Set<string>();
725
+ const knownUnitTypes = new Set([
726
+ "research-milestone", "plan-milestone", "research-slice", "plan-slice",
727
+ "execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
728
+ "run-uat", "fix-merge", "complete-milestone",
729
+ ]);
730
+ for (const hook of preferences.post_unit_hooks) {
731
+ if (!hook || typeof hook !== "object") {
732
+ errors.push("post_unit_hooks entry must be an object");
733
+ continue;
734
+ }
735
+ const name = typeof hook.name === "string" ? hook.name.trim() : "";
736
+ if (!name) {
737
+ errors.push("post_unit_hooks entry missing name");
738
+ continue;
739
+ }
740
+ if (seenNames.has(name)) {
741
+ errors.push(`duplicate post_unit_hooks name: ${name}`);
742
+ continue;
743
+ }
744
+ const after = normalizeStringList(hook.after);
745
+ if (after.length === 0) {
746
+ errors.push(`post_unit_hooks "${name}" missing after`);
747
+ continue;
748
+ }
749
+ for (const ut of after) {
750
+ if (!knownUnitTypes.has(ut)) {
751
+ errors.push(`post_unit_hooks "${name}" unknown unit type in after: ${ut}`);
752
+ }
753
+ }
754
+ const prompt = typeof hook.prompt === "string" ? hook.prompt.trim() : "";
755
+ if (!prompt) {
756
+ errors.push(`post_unit_hooks "${name}" missing prompt`);
757
+ continue;
758
+ }
759
+ const validHook: PostUnitHookConfig = { name, after, prompt };
760
+ if (hook.max_cycles !== undefined) {
761
+ const mc = typeof hook.max_cycles === "number" ? hook.max_cycles : Number(hook.max_cycles);
762
+ validHook.max_cycles = Number.isFinite(mc) ? Math.max(1, Math.min(10, Math.round(mc))) : 1;
763
+ }
764
+ if (typeof hook.model === "string" && hook.model.trim()) {
765
+ validHook.model = hook.model.trim();
766
+ }
767
+ if (typeof hook.artifact === "string" && hook.artifact.trim()) {
768
+ validHook.artifact = hook.artifact.trim();
769
+ }
770
+ if (typeof hook.retry_on === "string" && hook.retry_on.trim()) {
771
+ validHook.retry_on = hook.retry_on.trim();
772
+ }
773
+ if (typeof hook.agent === "string" && hook.agent.trim()) {
774
+ validHook.agent = hook.agent.trim();
775
+ }
776
+ if (hook.enabled !== undefined) {
777
+ validHook.enabled = !!hook.enabled;
778
+ }
779
+ seenNames.add(name);
780
+ validHooks.push(validHook);
781
+ }
782
+ if (validHooks.length > 0) {
783
+ validated.post_unit_hooks = validHooks;
784
+ }
785
+ }
786
+
787
+ // ─── Pre-Dispatch Hooks ─────────────────────────────────────────────────
788
+ if (preferences.pre_dispatch_hooks && Array.isArray(preferences.pre_dispatch_hooks)) {
789
+ const validPreHooks: PreDispatchHookConfig[] = [];
790
+ const seenPreNames = new Set<string>();
791
+ const knownUnitTypes = new Set([
792
+ "research-milestone", "plan-milestone", "research-slice", "plan-slice",
793
+ "execute-task", "complete-slice", "replan-slice", "reassess-roadmap",
794
+ "run-uat", "fix-merge", "complete-milestone",
795
+ ]);
796
+ const validActions = new Set(["modify", "skip", "replace"]);
797
+ for (const hook of preferences.pre_dispatch_hooks) {
798
+ if (!hook || typeof hook !== "object") {
799
+ errors.push("pre_dispatch_hooks entry must be an object");
800
+ continue;
801
+ }
802
+ const name = typeof hook.name === "string" ? hook.name.trim() : "";
803
+ if (!name) {
804
+ errors.push("pre_dispatch_hooks entry missing name");
805
+ continue;
806
+ }
807
+ if (seenPreNames.has(name)) {
808
+ errors.push(`duplicate pre_dispatch_hooks name: ${name}`);
809
+ continue;
810
+ }
811
+ const before = normalizeStringList(hook.before);
812
+ if (before.length === 0) {
813
+ errors.push(`pre_dispatch_hooks "${name}" missing before`);
814
+ continue;
815
+ }
816
+ for (const ut of before) {
817
+ if (!knownUnitTypes.has(ut)) {
818
+ errors.push(`pre_dispatch_hooks "${name}" unknown unit type in before: ${ut}`);
819
+ }
820
+ }
821
+ const action = typeof hook.action === "string" ? hook.action.trim() : "";
822
+ if (!validActions.has(action)) {
823
+ errors.push(`pre_dispatch_hooks "${name}" invalid action: ${action} (must be modify, skip, or replace)`);
824
+ continue;
825
+ }
826
+ const validHook: PreDispatchHookConfig = { name, before, action: action as PreDispatchHookConfig["action"] };
827
+ if (typeof hook.prepend === "string" && hook.prepend.trim()) validHook.prepend = hook.prepend.trim();
828
+ if (typeof hook.append === "string" && hook.append.trim()) validHook.append = hook.append.trim();
829
+ if (typeof hook.prompt === "string" && hook.prompt.trim()) validHook.prompt = hook.prompt.trim();
830
+ if (typeof hook.unit_type === "string" && hook.unit_type.trim()) validHook.unit_type = hook.unit_type.trim();
831
+ if (typeof hook.skip_if === "string" && hook.skip_if.trim()) validHook.skip_if = hook.skip_if.trim();
832
+ if (typeof hook.model === "string" && hook.model.trim()) validHook.model = hook.model.trim();
833
+ if (hook.enabled !== undefined) validHook.enabled = !!hook.enabled;
834
+
835
+ // Validation: action-specific required fields
836
+ if (action === "replace" && !validHook.prompt) {
837
+ errors.push(`pre_dispatch_hooks "${name}" action "replace" requires prompt`);
838
+ continue;
839
+ }
840
+ if (action === "modify" && !validHook.prepend && !validHook.append) {
841
+ errors.push(`pre_dispatch_hooks "${name}" action "modify" requires prepend or append`);
842
+ continue;
843
+ }
844
+
845
+ seenPreNames.add(name);
846
+ validPreHooks.push(validHook);
847
+ }
848
+ if (validPreHooks.length > 0) {
849
+ validated.pre_dispatch_hooks = validPreHooks;
850
+ }
851
+ }
852
+
853
+ // ─── Git Preferences ───────────────────────────────────────────────────
854
+ if (preferences.git && typeof preferences.git === "object") {
855
+ const git: Record<string, unknown> = {};
856
+ const g = preferences.git as Record<string, unknown>;
857
+
858
+ if (g.auto_push !== undefined) {
859
+ if (typeof g.auto_push === "boolean") git.auto_push = g.auto_push;
860
+ else errors.push("git.auto_push must be a boolean");
861
+ }
862
+ if (g.push_branches !== undefined) {
863
+ if (typeof g.push_branches === "boolean") git.push_branches = g.push_branches;
864
+ else errors.push("git.push_branches must be a boolean");
865
+ }
866
+ if (g.remote !== undefined) {
867
+ if (typeof g.remote === "string" && g.remote.trim() !== "") git.remote = g.remote.trim();
868
+ else errors.push("git.remote must be a non-empty string");
869
+ }
870
+ if (g.snapshots !== undefined) {
871
+ if (typeof g.snapshots === "boolean") git.snapshots = g.snapshots;
872
+ else errors.push("git.snapshots must be a boolean");
873
+ }
874
+ if (g.pre_merge_check !== undefined) {
875
+ if (typeof g.pre_merge_check === "boolean") {
876
+ git.pre_merge_check = g.pre_merge_check;
877
+ } else if (typeof g.pre_merge_check === "string" && g.pre_merge_check.trim() !== "") {
878
+ git.pre_merge_check = g.pre_merge_check.trim();
879
+ } else {
880
+ errors.push("git.pre_merge_check must be a boolean or a non-empty string command");
881
+ }
882
+ }
883
+ if (g.commit_type !== undefined) {
884
+ const validCommitTypes = new Set([
885
+ "feat", "fix", "refactor", "docs", "test", "chore", "perf", "ci", "build", "style",
886
+ ]);
887
+ if (typeof g.commit_type === "string" && validCommitTypes.has(g.commit_type)) {
888
+ git.commit_type = g.commit_type;
889
+ } else {
890
+ errors.push(`git.commit_type must be one of: feat, fix, refactor, docs, test, chore, perf, ci, build, style`);
891
+ }
892
+ }
893
+ if (g.merge_strategy !== undefined) {
894
+ const validStrategies = new Set(["squash", "merge"]);
895
+ if (typeof g.merge_strategy === "string" && validStrategies.has(g.merge_strategy)) {
896
+ git.merge_strategy = g.merge_strategy as "squash" | "merge";
897
+ } else {
898
+ errors.push("git.merge_strategy must be one of: squash, merge");
899
+ }
900
+ }
901
+ if (g.main_branch !== undefined) {
902
+ if (typeof g.main_branch === "string" && g.main_branch.trim() !== "" && VALID_BRANCH_NAME.test(g.main_branch)) {
903
+ git.main_branch = g.main_branch;
904
+ } else {
905
+ errors.push("git.main_branch must be a valid branch name (alphanumeric, _, -, /, .)");
906
+ }
907
+ }
908
+
909
+ if (Object.keys(git).length > 0) {
910
+ validated.git = git as GitPreferences;
911
+ }
912
+ }
913
+
914
+ return { preferences: validated, errors };
915
+ }
916
+
917
+ function mergeStringLists(base?: unknown, override?: unknown): string[] | undefined {
918
+ const merged = [
919
+ ...normalizeStringList(base),
920
+ ...normalizeStringList(override),
921
+ ]
922
+ .map((item) => item.trim())
923
+ .filter(Boolean);
924
+ return merged.length > 0 ? Array.from(new Set(merged)) : undefined;
925
+ }
926
+
927
+ function normalizeStringList(value: unknown): string[] {
928
+ if (!Array.isArray(value)) return [];
929
+ return value
930
+ .filter((item): item is string => typeof item === "string")
931
+ .map((item) => item.trim())
932
+ .filter(Boolean);
933
+ }
934
+
935
+ function mergePostUnitHooks(
936
+ base?: PostUnitHookConfig[],
937
+ override?: PostUnitHookConfig[],
938
+ ): PostUnitHookConfig[] | undefined {
939
+ if (!base?.length && !override?.length) return undefined;
940
+ const merged = [...(base ?? [])];
941
+ for (const hook of override ?? []) {
942
+ // Override hooks with same name replace base hooks
943
+ const idx = merged.findIndex(h => h.name === hook.name);
944
+ if (idx >= 0) {
945
+ merged[idx] = hook;
946
+ } else {
947
+ merged.push(hook);
948
+ }
949
+ }
950
+ return merged.length > 0 ? merged : undefined;
951
+ }
952
+
953
+ /**
954
+ * Resolve enabled post-unit hooks from effective preferences.
955
+ * Returns an empty array when no hooks are configured.
956
+ */
957
+ export function resolvePostUnitHooks(): PostUnitHookConfig[] {
958
+ const prefs = loadEffectiveGSDPreferences();
959
+ return (prefs?.preferences.post_unit_hooks ?? [])
960
+ .filter(h => h.enabled !== false);
961
+ }
962
+
963
+ function mergePreDispatchHooks(
964
+ base?: PreDispatchHookConfig[],
965
+ override?: PreDispatchHookConfig[],
966
+ ): PreDispatchHookConfig[] | undefined {
967
+ if (!base?.length && !override?.length) return undefined;
968
+ const merged = [...(base ?? [])];
969
+ for (const hook of override ?? []) {
970
+ const idx = merged.findIndex(h => h.name === hook.name);
971
+ if (idx >= 0) {
972
+ merged[idx] = hook;
973
+ } else {
974
+ merged.push(hook);
975
+ }
976
+ }
977
+ return merged.length > 0 ? merged : undefined;
978
+ }
979
+
980
+ /**
981
+ * Resolve enabled pre-dispatch hooks from effective preferences.
982
+ * Returns an empty array when no hooks are configured.
983
+ */
984
+ export function resolvePreDispatchHooks(): PreDispatchHookConfig[] {
985
+ const prefs = loadEffectiveGSDPreferences();
986
+ return (prefs?.preferences.pre_dispatch_hooks ?? [])
987
+ .filter(h => h.enabled !== false);
988
+ }