@tuan_son.dinh/gsd 2.6.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 (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +453 -0
  3. package/dist/app-paths.d.ts +4 -0
  4. package/dist/app-paths.js +6 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +269 -0
  7. package/dist/loader.d.ts +2 -0
  8. package/dist/loader.js +70 -0
  9. package/dist/logo.d.ts +16 -0
  10. package/dist/logo.js +25 -0
  11. package/dist/onboarding.d.ts +43 -0
  12. package/dist/onboarding.js +418 -0
  13. package/dist/pi-migration.d.ts +14 -0
  14. package/dist/pi-migration.js +57 -0
  15. package/dist/resource-loader.d.ts +22 -0
  16. package/dist/resource-loader.js +60 -0
  17. package/dist/tool-bootstrap.d.ts +4 -0
  18. package/dist/tool-bootstrap.js +74 -0
  19. package/dist/wizard.d.ts +7 -0
  20. package/dist/wizard.js +25 -0
  21. package/package.json +60 -0
  22. package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +108 -0
  23. package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -0
  24. package/pkg/dist/modes/interactive/theme/dark.json +85 -0
  25. package/pkg/dist/modes/interactive/theme/light.json +84 -0
  26. package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
  27. package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
  28. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  29. package/pkg/dist/modes/interactive/theme/theme.js +949 -0
  30. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
  31. package/pkg/package.json +8 -0
  32. package/scripts/postinstall.js +127 -0
  33. package/src/resources/GSD-WORKFLOW.md +661 -0
  34. package/src/resources/agents/researcher.md +29 -0
  35. package/src/resources/agents/scout.md +56 -0
  36. package/src/resources/agents/worker.md +31 -0
  37. package/src/resources/extensions/ask-user-questions.ts +249 -0
  38. package/src/resources/extensions/bg-shell/index.ts +2808 -0
  39. package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
  40. package/src/resources/extensions/browser-tools/core.js +1057 -0
  41. package/src/resources/extensions/browser-tools/index.ts +4989 -0
  42. package/src/resources/extensions/browser-tools/package.json +20 -0
  43. package/src/resources/extensions/context7/index.ts +428 -0
  44. package/src/resources/extensions/context7/package.json +11 -0
  45. package/src/resources/extensions/get-secrets-from-user.ts +352 -0
  46. package/src/resources/extensions/google-search/index.ts +323 -0
  47. package/src/resources/extensions/google-search/package.json +9 -0
  48. package/src/resources/extensions/gsd/activity-log.ts +69 -0
  49. package/src/resources/extensions/gsd/auto.ts +2744 -0
  50. package/src/resources/extensions/gsd/commands.ts +313 -0
  51. package/src/resources/extensions/gsd/crash-recovery.ts +85 -0
  52. package/src/resources/extensions/gsd/dashboard-overlay.ts +521 -0
  53. package/src/resources/extensions/gsd/docs/preferences-reference.md +176 -0
  54. package/src/resources/extensions/gsd/doctor.ts +690 -0
  55. package/src/resources/extensions/gsd/files.ts +732 -0
  56. package/src/resources/extensions/gsd/git-service.ts +597 -0
  57. package/src/resources/extensions/gsd/gitignore.ts +168 -0
  58. package/src/resources/extensions/gsd/guided-flow.ts +817 -0
  59. package/src/resources/extensions/gsd/index.ts +558 -0
  60. package/src/resources/extensions/gsd/metrics.ts +374 -0
  61. package/src/resources/extensions/gsd/migrate/command.ts +218 -0
  62. package/src/resources/extensions/gsd/migrate/index.ts +42 -0
  63. package/src/resources/extensions/gsd/migrate/parser.ts +323 -0
  64. package/src/resources/extensions/gsd/migrate/parsers.ts +624 -0
  65. package/src/resources/extensions/gsd/migrate/preview.ts +48 -0
  66. package/src/resources/extensions/gsd/migrate/transformer.ts +346 -0
  67. package/src/resources/extensions/gsd/migrate/types.ts +370 -0
  68. package/src/resources/extensions/gsd/migrate/validator.ts +55 -0
  69. package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
  70. package/src/resources/extensions/gsd/observability-validator.ts +408 -0
  71. package/src/resources/extensions/gsd/package.json +11 -0
  72. package/src/resources/extensions/gsd/paths.ts +308 -0
  73. package/src/resources/extensions/gsd/preferences.ts +757 -0
  74. package/src/resources/extensions/gsd/prompt-loader.ts +50 -0
  75. package/src/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
  76. package/src/resources/extensions/gsd/prompts/complete-slice.md +29 -0
  77. package/src/resources/extensions/gsd/prompts/discuss.md +189 -0
  78. package/src/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
  79. package/src/resources/extensions/gsd/prompts/execute-task.md +61 -0
  80. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -0
  81. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -0
  82. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +59 -0
  83. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -0
  84. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +23 -0
  85. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -0
  86. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +11 -0
  87. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
  88. package/src/resources/extensions/gsd/prompts/plan-milestone.md +65 -0
  89. package/src/resources/extensions/gsd/prompts/plan-slice.md +51 -0
  90. package/src/resources/extensions/gsd/prompts/queue.md +85 -0
  91. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
  92. package/src/resources/extensions/gsd/prompts/replan-slice.md +39 -0
  93. package/src/resources/extensions/gsd/prompts/research-milestone.md +37 -0
  94. package/src/resources/extensions/gsd/prompts/research-slice.md +28 -0
  95. package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
  96. package/src/resources/extensions/gsd/prompts/run-uat.md +109 -0
  97. package/src/resources/extensions/gsd/prompts/system.md +187 -0
  98. package/src/resources/extensions/gsd/prompts/worktree-merge.md +123 -0
  99. package/src/resources/extensions/gsd/session-forensics.ts +487 -0
  100. package/src/resources/extensions/gsd/skill-discovery.ts +137 -0
  101. package/src/resources/extensions/gsd/state.ts +460 -0
  102. package/src/resources/extensions/gsd/templates/context.md +76 -0
  103. package/src/resources/extensions/gsd/templates/decisions.md +8 -0
  104. package/src/resources/extensions/gsd/templates/milestone-summary.md +73 -0
  105. package/src/resources/extensions/gsd/templates/plan.md +131 -0
  106. package/src/resources/extensions/gsd/templates/preferences.md +24 -0
  107. package/src/resources/extensions/gsd/templates/project.md +31 -0
  108. package/src/resources/extensions/gsd/templates/reassessment.md +28 -0
  109. package/src/resources/extensions/gsd/templates/requirements.md +81 -0
  110. package/src/resources/extensions/gsd/templates/research.md +46 -0
  111. package/src/resources/extensions/gsd/templates/roadmap.md +118 -0
  112. package/src/resources/extensions/gsd/templates/slice-context.md +58 -0
  113. package/src/resources/extensions/gsd/templates/slice-summary.md +99 -0
  114. package/src/resources/extensions/gsd/templates/state.md +19 -0
  115. package/src/resources/extensions/gsd/templates/task-plan.md +52 -0
  116. package/src/resources/extensions/gsd/templates/task-summary.md +57 -0
  117. package/src/resources/extensions/gsd/templates/uat.md +54 -0
  118. package/src/resources/extensions/gsd/tests/activity-log-prune.test.ts +327 -0
  119. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +56 -0
  120. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +53 -0
  121. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +225 -0
  122. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +160 -0
  123. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +341 -0
  124. package/src/resources/extensions/gsd/tests/derive-state.test.ts +689 -0
  125. package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
  126. package/src/resources/extensions/gsd/tests/doctor.test.ts +505 -0
  127. package/src/resources/extensions/gsd/tests/git-service.test.ts +1313 -0
  128. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +308 -0
  129. package/src/resources/extensions/gsd/tests/metrics-io.test.ts +201 -0
  130. package/src/resources/extensions/gsd/tests/metrics.test.ts +217 -0
  131. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +390 -0
  132. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +786 -0
  133. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +657 -0
  134. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +443 -0
  135. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +318 -0
  136. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +420 -0
  137. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +309 -0
  138. package/src/resources/extensions/gsd/tests/parsers.test.ts +1351 -0
  139. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +163 -0
  140. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +386 -0
  141. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +171 -0
  142. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
  143. package/src/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
  144. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +521 -0
  145. package/src/resources/extensions/gsd/tests/requirements.test.ts +125 -0
  146. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +34 -0
  147. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +11 -0
  148. package/src/resources/extensions/gsd/tests/run-uat.test.ts +348 -0
  149. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +247 -0
  150. package/src/resources/extensions/gsd/tests/workflow-config.test.mjs +53 -0
  151. package/src/resources/extensions/gsd/tests/workspace-index.test.ts +94 -0
  152. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +253 -0
  153. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
  154. package/src/resources/extensions/gsd/tests/worktree.test.ts +264 -0
  155. package/src/resources/extensions/gsd/types.ts +159 -0
  156. package/src/resources/extensions/gsd/unit-runtime.ts +184 -0
  157. package/src/resources/extensions/gsd/workspace-index.ts +203 -0
  158. package/src/resources/extensions/gsd/worktree-command.ts +845 -0
  159. package/src/resources/extensions/gsd/worktree-manager.ts +392 -0
  160. package/src/resources/extensions/gsd/worktree.ts +183 -0
  161. package/src/resources/extensions/mac-tools/index.ts +852 -0
  162. package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
  163. package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
  164. package/src/resources/extensions/mcporter/index.ts +429 -0
  165. package/src/resources/extensions/remote-questions/config.ts +81 -0
  166. package/src/resources/extensions/remote-questions/discord-adapter.ts +128 -0
  167. package/src/resources/extensions/remote-questions/format.ts +163 -0
  168. package/src/resources/extensions/remote-questions/manager.ts +192 -0
  169. package/src/resources/extensions/remote-questions/remote-command.ts +307 -0
  170. package/src/resources/extensions/remote-questions/slack-adapter.ts +92 -0
  171. package/src/resources/extensions/remote-questions/status.ts +31 -0
  172. package/src/resources/extensions/remote-questions/store.ts +77 -0
  173. package/src/resources/extensions/remote-questions/types.ts +75 -0
  174. package/src/resources/extensions/search-the-web/cache.ts +78 -0
  175. package/src/resources/extensions/search-the-web/command-search-provider.ts +95 -0
  176. package/src/resources/extensions/search-the-web/format.ts +258 -0
  177. package/src/resources/extensions/search-the-web/http.ts +238 -0
  178. package/src/resources/extensions/search-the-web/index.ts +65 -0
  179. package/src/resources/extensions/search-the-web/native-search.ts +157 -0
  180. package/src/resources/extensions/search-the-web/provider.ts +118 -0
  181. package/src/resources/extensions/search-the-web/tavily.ts +116 -0
  182. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
  183. package/src/resources/extensions/search-the-web/tool-llm-context.ts +561 -0
  184. package/src/resources/extensions/search-the-web/tool-search.ts +576 -0
  185. package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
  186. package/src/resources/extensions/shared/confirm-ui.ts +126 -0
  187. package/src/resources/extensions/shared/interview-ui.ts +613 -0
  188. package/src/resources/extensions/shared/next-action-ui.ts +197 -0
  189. package/src/resources/extensions/shared/progress-widget.ts +282 -0
  190. package/src/resources/extensions/shared/terminal.ts +23 -0
  191. package/src/resources/extensions/shared/thinking-widget.ts +107 -0
  192. package/src/resources/extensions/shared/ui.ts +400 -0
  193. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  194. package/src/resources/extensions/slash-commands/audit.ts +88 -0
  195. package/src/resources/extensions/slash-commands/clear.ts +10 -0
  196. package/src/resources/extensions/slash-commands/create-extension.ts +297 -0
  197. package/src/resources/extensions/slash-commands/create-slash-command.ts +234 -0
  198. package/src/resources/extensions/slash-commands/index.ts +12 -0
  199. package/src/resources/extensions/subagent/agents.ts +126 -0
  200. package/src/resources/extensions/subagent/index.ts +1020 -0
  201. package/src/resources/extensions/voice/index.ts +195 -0
  202. package/src/resources/extensions/voice/speech-recognizer.swift +154 -0
  203. package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
  204. package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
  205. package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
  206. package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
  207. package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
  208. package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
  209. package/src/resources/skills/frontend-design/SKILL.md +45 -0
  210. package/src/resources/skills/swiftui/SKILL.md +208 -0
  211. package/src/resources/skills/swiftui/references/animations.md +921 -0
  212. package/src/resources/skills/swiftui/references/architecture.md +1561 -0
  213. package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
  214. package/src/resources/skills/swiftui/references/navigation.md +1492 -0
  215. package/src/resources/skills/swiftui/references/networking-async.md +214 -0
  216. package/src/resources/skills/swiftui/references/performance.md +1706 -0
  217. package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
  218. package/src/resources/skills/swiftui/references/state-management.md +1443 -0
  219. package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
  220. package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
  221. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
  222. package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
  223. package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
  224. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
  225. package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
  226. package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
  227. package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
@@ -0,0 +1,558 @@
1
+ /**
2
+ * GSD Extension — /gsd
3
+ *
4
+ * One command, one wizard. Reads state from disk, shows contextual options,
5
+ * dispatches through GSD-WORKFLOW.md. The LLM does the rest.
6
+ *
7
+ * Auto-mode: /gsd auto loops fresh sessions until milestone complete.
8
+ *
9
+ * Commands:
10
+ * /gsd — contextual wizard (smart entry point)
11
+ * /gsd auto — start auto-mode (fresh session per unit)
12
+ * /gsd stop — stop auto-mode gracefully
13
+ * /gsd status — progress dashboard
14
+ *
15
+ * Hooks:
16
+ * before_agent_start — inject GSD system context for GSD projects
17
+ * agent_end — auto-mode advancement
18
+ * session_before_compact — save continue.md OR block during auto
19
+ */
20
+
21
+ import type {
22
+ ExtensionAPI,
23
+ ExtensionContext,
24
+ } from "@mariozechner/pi-coding-agent";
25
+ import { createBashTool, createWriteTool, createReadTool, createEditTool } from "@mariozechner/pi-coding-agent";
26
+
27
+ import { registerGSDCommand } from "./commands.js";
28
+ import { registerWorktreeCommand, getWorktreeOriginalCwd, getActiveWorktreeName } from "./worktree-command.js";
29
+ import { saveFile, formatContinue, loadFile, parseContinue, parseSummary } from "./files.js";
30
+ import { loadPrompt } from "./prompt-loader.js";
31
+ import { deriveState } from "./state.js";
32
+ import { isAutoActive, isAutoPaused, handleAgentEnd, pauseAuto, getAutoDashboardData } from "./auto.js";
33
+ import { saveActivityLog } from "./activity-log.js";
34
+ import { checkAutoStartAfterDiscuss } from "./guided-flow.js";
35
+ import { GSDDashboardOverlay } from "./dashboard-overlay.js";
36
+ import {
37
+ loadEffectiveGSDPreferences,
38
+ renderPreferencesForSystemPrompt,
39
+ resolveAllSkillReferences,
40
+ } from "./preferences.js";
41
+ import { hasSkillSnapshot, detectNewSkills, formatSkillsXml } from "./skill-discovery.js";
42
+ import {
43
+ resolveSlicePath, resolveSliceFile, resolveTaskFile, resolveTaskFiles, resolveTasksDir,
44
+ relSliceFile, relSlicePath, relTaskFile,
45
+ buildSliceFileName, gsdRoot,
46
+ } from "./paths.js";
47
+ import { Key } from "@mariozechner/pi-tui";
48
+ import { join } from "node:path";
49
+ import { existsSync } from "node:fs";
50
+ import { shortcutDesc } from "../shared/terminal.js";
51
+ import { Text } from "@mariozechner/pi-tui";
52
+
53
+ // ── ASCII logo ────────────────────────────────────────────────────────────
54
+ const GSD_LOGO_LINES = [
55
+ " ██████╗ ███████╗██████╗ ",
56
+ " ██╔════╝ ██╔════╝██╔══██╗",
57
+ " ██║ ███╗███████╗██║ ██║",
58
+ " ██║ ██║╚════██║██║ ██║",
59
+ " ╚██████╔╝███████║██████╔╝",
60
+ " ╚═════╝ ╚══════╝╚═════╝ ",
61
+ ];
62
+
63
+ export default function (pi: ExtensionAPI) {
64
+ registerGSDCommand(pi);
65
+ registerWorktreeCommand(pi);
66
+
67
+ // ── /exit — kill the process immediately ──────────────────────────────
68
+ pi.registerCommand("exit", {
69
+ description: "Exit GSD immediately",
70
+ handler: async (_ctx) => {
71
+ process.exit(0);
72
+ },
73
+ });
74
+
75
+ // ── Dynamic-cwd bash tool with default timeout ────────────────────────
76
+ // The built-in bash tool captures cwd at startup. This replacement uses
77
+ // a spawnHook to read process.cwd() dynamically so that process.chdir()
78
+ // (used by /worktree switch) propagates to shell commands.
79
+ //
80
+ // The upstream SDK's bash tool has no default timeout — if the LLM omits
81
+ // the timeout parameter, commands run indefinitely, causing hangs on
82
+ // Windows where process killing is unreliable (see #40). We wrap execute
83
+ // to inject a 120-second default when no timeout is provided.
84
+ const DEFAULT_BASH_TIMEOUT_SECS = 120;
85
+ const baseBash = createBashTool(process.cwd(), {
86
+ spawnHook: (ctx) => ({ ...ctx, cwd: process.cwd() }),
87
+ });
88
+ const dynamicBash = {
89
+ ...baseBash,
90
+ execute: async (
91
+ toolCallId: string,
92
+ params: { command: string; timeout?: number },
93
+ signal?: AbortSignal,
94
+ onUpdate?: any,
95
+ ctx?: any,
96
+ ) => {
97
+ const paramsWithTimeout = {
98
+ ...params,
99
+ timeout: params.timeout ?? DEFAULT_BASH_TIMEOUT_SECS,
100
+ };
101
+ return baseBash.execute(toolCallId, paramsWithTimeout, signal, onUpdate, ctx);
102
+ },
103
+ };
104
+ pi.registerTool(dynamicBash as any);
105
+
106
+ // ── Dynamic-cwd file tools (write, read, edit) ────────────────────────
107
+ // The built-in file tools capture cwd at startup. When process.chdir()
108
+ // moves us into a worktree, relative paths still resolve against the
109
+ // original launch directory. These replacements delegate to freshly-
110
+ // created tools on each call so that process.cwd() is read dynamically.
111
+ const baseWrite = createWriteTool(process.cwd());
112
+ const dynamicWrite = {
113
+ ...baseWrite,
114
+ execute: async (
115
+ toolCallId: string,
116
+ params: { path: string; content: string },
117
+ signal?: AbortSignal,
118
+ onUpdate?: any,
119
+ ctx?: any,
120
+ ) => {
121
+ const fresh = createWriteTool(process.cwd());
122
+ return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
123
+ },
124
+ };
125
+ pi.registerTool(dynamicWrite as any);
126
+
127
+ const baseRead = createReadTool(process.cwd());
128
+ const dynamicRead = {
129
+ ...baseRead,
130
+ execute: async (
131
+ toolCallId: string,
132
+ params: { path: string; offset?: number; limit?: number },
133
+ signal?: AbortSignal,
134
+ onUpdate?: any,
135
+ ctx?: any,
136
+ ) => {
137
+ const fresh = createReadTool(process.cwd());
138
+ return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
139
+ },
140
+ };
141
+ pi.registerTool(dynamicRead as any);
142
+
143
+ const baseEdit = createEditTool(process.cwd());
144
+ const dynamicEdit = {
145
+ ...baseEdit,
146
+ execute: async (
147
+ toolCallId: string,
148
+ params: { path: string; oldText: string; newText: string },
149
+ signal?: AbortSignal,
150
+ onUpdate?: any,
151
+ ctx?: any,
152
+ ) => {
153
+ const fresh = createEditTool(process.cwd());
154
+ return fresh.execute(toolCallId, params, signal, onUpdate, ctx);
155
+ },
156
+ };
157
+ pi.registerTool(dynamicEdit as any);
158
+
159
+ // ── session_start: render branded GSD header + remote channel status ──
160
+ pi.on("session_start", async (_event, ctx) => {
161
+ // Theme access throws in RPC mode (no TUI) — header is decorative, skip it
162
+ try {
163
+ const theme = ctx.ui.theme;
164
+ const version = process.env.GSD_VERSION || "0.0.0";
165
+
166
+ const logoText = GSD_LOGO_LINES.map((line) => theme.fg("accent", line)).join("\n");
167
+ const titleLine = ` ${theme.bold("Get Shit Done")} ${theme.fg("dim", `v${version}`)}`;
168
+
169
+ const headerContent = `${logoText}\n${titleLine}`;
170
+ ctx.ui.setHeader((_ui, _theme) => new Text(headerContent, 1, 0));
171
+ } catch {
172
+ // RPC mode — no TUI, skip header rendering
173
+ }
174
+
175
+ // Notify remote questions status if configured
176
+ try {
177
+ const [{ getRemoteConfigStatus }, { getLatestPromptSummary }] = await Promise.all([
178
+ import("../remote-questions/config.js"),
179
+ import("../remote-questions/status.js"),
180
+ ]);
181
+ const status = getRemoteConfigStatus();
182
+ const latest = getLatestPromptSummary();
183
+ if (!status.includes("not configured")) {
184
+ const suffix = latest ? `\nLast remote prompt: ${latest.id} (${latest.status})` : "";
185
+ ctx.ui.notify(`${status}${suffix}`, status.includes("disabled") ? "warning" : "info");
186
+ }
187
+ } catch {
188
+ // Remote questions module not available — ignore
189
+ }
190
+ });
191
+
192
+ // ── Ctrl+Alt+G shortcut — GSD dashboard overlay ────────────────────────
193
+ pi.registerShortcut(Key.ctrlAlt("g"), {
194
+ description: shortcutDesc("Open GSD dashboard", "/gsd status"),
195
+ handler: async (ctx) => {
196
+ // Only show if .gsd/ exists
197
+ if (!existsSync(join(process.cwd(), ".gsd"))) {
198
+ ctx.ui.notify("No .gsd/ directory found. Run /gsd to start.", "info");
199
+ return;
200
+ }
201
+
202
+ await ctx.ui.custom<void>(
203
+ (tui, theme, _kb, done) => {
204
+ return new GSDDashboardOverlay(tui, theme, () => done());
205
+ },
206
+ {
207
+ overlay: true,
208
+ overlayOptions: {
209
+ width: "90%",
210
+ minWidth: 80,
211
+ maxHeight: "92%",
212
+ anchor: "center",
213
+ },
214
+ },
215
+ );
216
+ },
217
+ });
218
+
219
+ // ── before_agent_start: inject GSD contract into true system prompt ─────
220
+ pi.on("before_agent_start", async (event, ctx: ExtensionContext) => {
221
+ if (!existsSync(join(process.cwd(), ".gsd"))) return;
222
+
223
+ const systemContent = loadPrompt("system");
224
+ const loadedPreferences = loadEffectiveGSDPreferences();
225
+ let preferenceBlock = "";
226
+ if (loadedPreferences) {
227
+ const cwd = process.cwd();
228
+ const report = resolveAllSkillReferences(loadedPreferences.preferences, cwd);
229
+ preferenceBlock = `\n\n${renderPreferencesForSystemPrompt(loadedPreferences.preferences, report.resolutions)}`;
230
+
231
+ // Emit warnings for unresolved skill references
232
+ if (report.warnings.length > 0) {
233
+ ctx.ui.notify(
234
+ `GSD skill preferences: ${report.warnings.length} unresolved skill${report.warnings.length === 1 ? "" : "s"}: ${report.warnings.join(", ")}`,
235
+ "warning",
236
+ );
237
+ }
238
+ }
239
+
240
+ // Detect skills installed during this auto-mode session
241
+ let newSkillsBlock = "";
242
+ if (hasSkillSnapshot()) {
243
+ const newSkills = detectNewSkills();
244
+ if (newSkills.length > 0) {
245
+ newSkillsBlock = formatSkillsXml(newSkills);
246
+ }
247
+ }
248
+
249
+ const injection = await buildGuidedExecuteContextInjection(event.prompt, process.cwd());
250
+
251
+ // Worktree context — override the static CWD in the system prompt
252
+ let worktreeBlock = "";
253
+ const worktreeName = getActiveWorktreeName();
254
+ const worktreeMainCwd = getWorktreeOriginalCwd();
255
+ if (worktreeName && worktreeMainCwd) {
256
+ worktreeBlock = [
257
+ "",
258
+ "",
259
+ "[WORKTREE CONTEXT — OVERRIDES CURRENT WORKING DIRECTORY ABOVE]",
260
+ `IMPORTANT: Ignore the "Current working directory" shown earlier in this prompt.`,
261
+ `The actual current working directory is: ${process.cwd()}`,
262
+ "",
263
+ `You are working inside a GSD worktree.`,
264
+ `- Worktree name: ${worktreeName}`,
265
+ `- Worktree path (this is the real cwd): ${process.cwd()}`,
266
+ `- Main project: ${worktreeMainCwd}`,
267
+ `- Branch: worktree/${worktreeName}`,
268
+ "",
269
+ "All file operations, bash commands, and GSD state resolve against the worktree path above.",
270
+ "Use /worktree merge to merge changes back. Use /worktree return to switch back to the main tree.",
271
+ ].join("\n");
272
+ }
273
+
274
+ return {
275
+ systemPrompt: `${event.systemPrompt}\n\n[SYSTEM CONTEXT — GSD]\n\n${systemContent}${preferenceBlock}${newSkillsBlock}${worktreeBlock}`,
276
+ ...(injection
277
+ ? {
278
+ message: {
279
+ customType: "gsd-guided-context",
280
+ content: injection,
281
+ display: false,
282
+ },
283
+ }
284
+ : {}),
285
+ };
286
+ });
287
+
288
+ // ── agent_end: auto-mode advancement or auto-start after discuss ───────────
289
+ pi.on("agent_end", async (event, ctx: ExtensionContext) => {
290
+ // If discuss phase just finished, start auto-mode
291
+ if (checkAutoStartAfterDiscuss()) return;
292
+
293
+ // If auto-mode is already running, advance to next unit
294
+ if (!isAutoActive()) return;
295
+
296
+ // If the agent was aborted (user pressed Escape), pause auto-mode
297
+ // instead of advancing. This preserves the conversation so the user
298
+ // can inspect what happened, interact with the agent, or resume.
299
+ const lastMsg = event.messages[event.messages.length - 1];
300
+ if (lastMsg && "stopReason" in lastMsg && lastMsg.stopReason === "aborted") {
301
+ await pauseAuto(ctx, pi);
302
+ return;
303
+ }
304
+
305
+ await handleAgentEnd(ctx, pi);
306
+ });
307
+
308
+ // ── session_before_compact ────────────────────────────────────────────────
309
+ pi.on("session_before_compact", async (_event, _ctx: ExtensionContext) => {
310
+ // Block compaction during auto-mode — each unit is a fresh session
311
+ // Also block during paused state — context is valuable for the user
312
+ if (isAutoActive() || isAutoPaused()) {
313
+ return { cancel: true };
314
+ }
315
+
316
+ const basePath = process.cwd();
317
+ const state = await deriveState(basePath);
318
+
319
+ // Only save continue.md if we're actively executing a task
320
+ if (!state.activeMilestone || !state.activeSlice || !state.activeTask) return;
321
+ if (state.phase !== "executing") return;
322
+
323
+ const sDir = resolveSlicePath(basePath, state.activeMilestone.id, state.activeSlice.id);
324
+ if (!sDir) return;
325
+
326
+ // Check for existing continue file (new naming or legacy)
327
+ const existingFile = resolveSliceFile(basePath, state.activeMilestone.id, state.activeSlice.id, "CONTINUE");
328
+ if (existingFile && await loadFile(existingFile)) return;
329
+ const legacyContinue = join(sDir, "continue.md");
330
+ if (await loadFile(legacyContinue)) return;
331
+
332
+ const continuePath = join(sDir, buildSliceFileName(state.activeSlice.id, "CONTINUE"));
333
+
334
+ const continueData = {
335
+ frontmatter: {
336
+ milestone: state.activeMilestone.id,
337
+ slice: state.activeSlice.id,
338
+ task: state.activeTask.id,
339
+ step: 0,
340
+ totalSteps: 0,
341
+ status: "compacted" as const,
342
+ savedAt: new Date().toISOString(),
343
+ },
344
+ completedWork: `Task ${state.activeTask.id} (${state.activeTask.title}) was in progress when compaction occurred.`,
345
+ remainingWork: "Check the task plan for remaining steps.",
346
+ decisions: "Check task summary files for prior decisions.",
347
+ context: "Session was auto-compacted by Pi. Resume with /gsd.",
348
+ nextAction: `Resume task ${state.activeTask.id}: ${state.activeTask.title}.`,
349
+ };
350
+
351
+ await saveFile(continuePath, formatContinue(continueData));
352
+ });
353
+
354
+ // ── session_shutdown: save activity log on Ctrl+C / SIGTERM ─────────────
355
+ pi.on("session_shutdown", async (_event, ctx: ExtensionContext) => {
356
+ if (!isAutoActive() && !isAutoPaused()) return;
357
+
358
+ // Save the current session — the lock file stays on disk
359
+ // so the next /gsd auto knows it was interrupted
360
+ const dash = getAutoDashboardData();
361
+ if (dash.currentUnit) {
362
+ saveActivityLog(ctx, dash.basePath, dash.currentUnit.type, dash.currentUnit.id);
363
+ }
364
+ });
365
+ }
366
+
367
+ async function buildGuidedExecuteContextInjection(prompt: string, basePath: string): Promise<string | null> {
368
+ const executeMatch = prompt.match(/Execute the next task:\s+(T\d+)\s+\("([^"]+)"\)\s+in slice\s+(S\d+)\s+of milestone\s+(M\d+)/i);
369
+ if (executeMatch) {
370
+ const [, taskId, taskTitle, sliceId, milestoneId] = executeMatch;
371
+ return buildTaskExecutionContextInjection(basePath, milestoneId, sliceId, taskId, taskTitle);
372
+ }
373
+
374
+ const resumeMatch = prompt.match(/Resume interrupted work\.[\s\S]*?slice\s+(S\d+)\s+of milestone\s+(M\d+)/i);
375
+ if (resumeMatch) {
376
+ const [, sliceId, milestoneId] = resumeMatch;
377
+ const state = await deriveState(basePath);
378
+ if (
379
+ state.activeMilestone?.id === milestoneId &&
380
+ state.activeSlice?.id === sliceId &&
381
+ state.activeTask
382
+ ) {
383
+ return buildTaskExecutionContextInjection(
384
+ basePath,
385
+ milestoneId,
386
+ sliceId,
387
+ state.activeTask.id,
388
+ state.activeTask.title,
389
+ );
390
+ }
391
+ }
392
+
393
+ return null;
394
+ }
395
+
396
+ async function buildTaskExecutionContextInjection(
397
+ basePath: string,
398
+ milestoneId: string,
399
+ sliceId: string,
400
+ taskId: string,
401
+ taskTitle: string,
402
+ ): Promise<string> {
403
+ const taskPlanPath = resolveTaskFile(basePath, milestoneId, sliceId, taskId, "PLAN");
404
+ const taskPlanRelPath = relTaskFile(basePath, milestoneId, sliceId, taskId, "PLAN");
405
+ const taskPlanContent = taskPlanPath ? await loadFile(taskPlanPath) : null;
406
+ const taskPlanInline = taskPlanContent
407
+ ? [
408
+ "## Inlined Task Plan (authoritative local execution contract)",
409
+ `Source: \`${taskPlanRelPath}\``,
410
+ "",
411
+ taskPlanContent.trim(),
412
+ ].join("\n")
413
+ : [
414
+ "## Inlined Task Plan (authoritative local execution contract)",
415
+ `Task plan not found at dispatch time. Read \`${taskPlanRelPath}\` before executing.`,
416
+ ].join("\n");
417
+
418
+ const slicePlanPath = resolveSliceFile(basePath, milestoneId, sliceId, "PLAN");
419
+ const slicePlanRelPath = relSliceFile(basePath, milestoneId, sliceId, "PLAN");
420
+ const slicePlanContent = slicePlanPath ? await loadFile(slicePlanPath) : null;
421
+ const slicePlanExcerpt = extractSliceExecutionExcerpt(slicePlanContent, slicePlanRelPath);
422
+
423
+ const priorTaskLines = await buildCarryForwardLines(basePath, milestoneId, sliceId, taskId);
424
+ const resumeSection = await buildResumeSection(basePath, milestoneId, sliceId);
425
+
426
+ return [
427
+ "[GSD Guided Execute Context]",
428
+ "Use this injected context as startup context for guided task execution. Treat the inlined task plan as the authoritative local execution contract. Use source artifacts to verify details and run checks.",
429
+ "",
430
+ resumeSection,
431
+ "",
432
+ "## Carry-Forward Context",
433
+ ...priorTaskLines,
434
+ "",
435
+ taskPlanInline,
436
+ "",
437
+ slicePlanExcerpt,
438
+ "",
439
+ "## Backing Source Artifacts",
440
+ `- Slice plan: \`${slicePlanRelPath}\``,
441
+ `- Task plan source: \`${taskPlanRelPath}\``,
442
+ ].join("\n");
443
+ }
444
+
445
+ async function buildCarryForwardLines(
446
+ basePath: string,
447
+ milestoneId: string,
448
+ sliceId: string,
449
+ taskId: string,
450
+ ): Promise<string[]> {
451
+ const tDir = resolveTasksDir(basePath, milestoneId, sliceId);
452
+ if (!tDir) return ["- No prior task summaries in this slice."];
453
+
454
+ const currentNum = parseInt(taskId.replace(/^T/, ""), 10);
455
+ const sRel = relSlicePath(basePath, milestoneId, sliceId);
456
+ const summaryFiles = resolveTaskFiles(tDir, "SUMMARY")
457
+ .filter((file) => parseInt(file.replace(/^T/, ""), 10) < currentNum)
458
+ .sort();
459
+
460
+ if (summaryFiles.length === 0) return ["- No prior task summaries in this slice."];
461
+
462
+ const lines = await Promise.all(summaryFiles.map(async (file) => {
463
+ const absPath = join(tDir, file);
464
+ const content = await loadFile(absPath);
465
+ const relPath = `${sRel}/tasks/${file}`;
466
+ if (!content) return `- \`${relPath}\``;
467
+
468
+ const summary = parseSummary(content);
469
+ const provided = summary.frontmatter.provides.slice(0, 2).join("; ");
470
+ const decisions = summary.frontmatter.key_decisions.slice(0, 2).join("; ");
471
+ const patterns = summary.frontmatter.patterns_established.slice(0, 2).join("; ");
472
+ const diagnostics = extractMarkdownSection(content, "Diagnostics");
473
+
474
+ const parts = [summary.title || relPath];
475
+ if (summary.oneLiner) parts.push(summary.oneLiner);
476
+ if (provided) parts.push(`provides: ${provided}`);
477
+ if (decisions) parts.push(`decisions: ${decisions}`);
478
+ if (patterns) parts.push(`patterns: ${patterns}`);
479
+ if (diagnostics) parts.push(`diagnostics: ${oneLine(diagnostics)}`);
480
+
481
+ return `- \`${relPath}\` — ${parts.join(" | ")}`;
482
+ }));
483
+
484
+ return lines;
485
+ }
486
+
487
+ async function buildResumeSection(basePath: string, milestoneId: string, sliceId: string): Promise<string> {
488
+ const continueFile = resolveSliceFile(basePath, milestoneId, sliceId, "CONTINUE");
489
+ const legacyDir = resolveSlicePath(basePath, milestoneId, sliceId);
490
+ const legacyPath = legacyDir ? join(legacyDir, "continue.md") : null;
491
+ const continueContent = continueFile ? await loadFile(continueFile) : null;
492
+ const legacyContent = !continueContent && legacyPath ? await loadFile(legacyPath) : null;
493
+ const resolvedContent = continueContent ?? legacyContent;
494
+ const resolvedRelPath = continueContent
495
+ ? relSliceFile(basePath, milestoneId, sliceId, "CONTINUE")
496
+ : (legacyPath ? `${relSlicePath(basePath, milestoneId, sliceId)}/continue.md` : null);
497
+
498
+ if (!resolvedContent || !resolvedRelPath) {
499
+ return ["## Resume State", "- No continue file present. Start from the top of the task plan."].join("\n");
500
+ }
501
+
502
+ const cont = parseContinue(resolvedContent);
503
+ const lines = [
504
+ "## Resume State",
505
+ `Source: \`${resolvedRelPath}\``,
506
+ `- Status: ${cont.frontmatter.status || "in_progress"}`,
507
+ ];
508
+
509
+ if (cont.frontmatter.step && cont.frontmatter.totalSteps) {
510
+ lines.push(`- Progress: step ${cont.frontmatter.step} of ${cont.frontmatter.totalSteps}`);
511
+ }
512
+ if (cont.completedWork) lines.push(`- Completed: ${oneLine(cont.completedWork)}`);
513
+ if (cont.remainingWork) lines.push(`- Remaining: ${oneLine(cont.remainingWork)}`);
514
+ if (cont.decisions) lines.push(`- Decisions: ${oneLine(cont.decisions)}`);
515
+ if (cont.nextAction) lines.push(`- Next action: ${oneLine(cont.nextAction)}`);
516
+
517
+ return lines.join("\n");
518
+ }
519
+
520
+ function extractSliceExecutionExcerpt(content: string | null, relPath: string): string {
521
+ if (!content) {
522
+ return [
523
+ "## Slice Plan Excerpt",
524
+ `Slice plan not found at dispatch time. Read \`${relPath}\` before running slice-level verification.`,
525
+ ].join("\n");
526
+ }
527
+
528
+ const lines = content.split("\n");
529
+ const goalLine = lines.find((line) => line.startsWith("**Goal:**"))?.trim();
530
+ const demoLine = lines.find((line) => line.startsWith("**Demo:**"))?.trim();
531
+ const verification = extractMarkdownSection(content, "Verification");
532
+ const observability = extractMarkdownSection(content, "Observability / Diagnostics");
533
+
534
+ const parts = ["## Slice Plan Excerpt", `Source: \`${relPath}\``];
535
+ if (goalLine) parts.push(goalLine);
536
+ if (demoLine) parts.push(demoLine);
537
+ if (verification) parts.push("", "### Slice Verification", verification.trim());
538
+ if (observability) parts.push("", "### Slice Observability / Diagnostics", observability.trim());
539
+ return parts.join("\n");
540
+ }
541
+
542
+ function extractMarkdownSection(content: string, heading: string): string | null {
543
+ const match = new RegExp(`^## ${escapeRegExp(heading)}\\s*$`, "m").exec(content);
544
+ if (!match) return null;
545
+ const start = match.index + match[0].length;
546
+ const rest = content.slice(start);
547
+ const nextHeading = rest.match(/^##\s+/m);
548
+ const end = nextHeading?.index ?? rest.length;
549
+ return rest.slice(0, end).trim();
550
+ }
551
+
552
+ function escapeRegExp(value: string): string {
553
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
554
+ }
555
+
556
+ function oneLine(text: string): string {
557
+ return text.replace(/\s+/g, " ").trim();
558
+ }