@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,374 @@
1
+ /**
2
+ * GSD Metrics — Token & Cost Tracking
3
+ *
4
+ * Accumulates per-unit usage data across auto-mode sessions.
5
+ * Data is extracted from session entries before each context wipe,
6
+ * written to .gsd/metrics.json, and surfaced in the dashboard.
7
+ *
8
+ * Data flow:
9
+ * 1. Before newSession() wipes context, snapshotUnitMetrics() scans
10
+ * session entries for AssistantMessage usage data
11
+ * 2. The unit record is appended to the in-memory ledger and flushed to disk
12
+ * 3. The dashboard overlay and progress widget read from the in-memory ledger
13
+ * 4. On crash recovery or fresh start, the ledger is loaded from disk
14
+ */
15
+
16
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
17
+ import { join } from "node:path";
18
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
19
+ import { gsdRoot } from "./paths.js";
20
+
21
+ // ─── Types ────────────────────────────────────────────────────────────────────
22
+
23
+ export interface TokenCounts {
24
+ input: number;
25
+ output: number;
26
+ cacheRead: number;
27
+ cacheWrite: number;
28
+ total: number;
29
+ }
30
+
31
+ export interface UnitMetrics {
32
+ type: string; // e.g. "research-milestone", "execute-task"
33
+ id: string; // e.g. "M001/S01/T01"
34
+ model: string; // model ID used
35
+ startedAt: number; // ms timestamp
36
+ finishedAt: number; // ms timestamp
37
+ tokens: TokenCounts;
38
+ cost: number; // total USD cost
39
+ toolCalls: number;
40
+ assistantMessages: number;
41
+ userMessages: number;
42
+ }
43
+
44
+ export interface MetricsLedger {
45
+ version: 1;
46
+ projectStartedAt: number;
47
+ units: UnitMetrics[];
48
+ }
49
+
50
+ // ─── Phase classification ─────────────────────────────────────────────────────
51
+
52
+ export type MetricsPhase = "research" | "planning" | "execution" | "completion" | "reassessment";
53
+
54
+ export function classifyUnitPhase(unitType: string): MetricsPhase {
55
+ switch (unitType) {
56
+ case "research-milestone":
57
+ case "research-slice":
58
+ return "research";
59
+ case "plan-milestone":
60
+ case "plan-slice":
61
+ return "planning";
62
+ case "execute-task":
63
+ return "execution";
64
+ case "complete-slice":
65
+ return "completion";
66
+ case "reassess-roadmap":
67
+ return "reassessment";
68
+ default:
69
+ return "execution";
70
+ }
71
+ }
72
+
73
+ // ─── In-memory state ──────────────────────────────────────────────────────────
74
+
75
+ let ledger: MetricsLedger | null = null;
76
+ let basePath: string = "";
77
+
78
+ // ─── Public API ───────────────────────────────────────────────────────────────
79
+
80
+ /**
81
+ * Initialize the metrics system for a given project.
82
+ * Loads existing ledger from disk if present.
83
+ */
84
+ export function initMetrics(base: string): void {
85
+ basePath = base;
86
+ ledger = loadLedger(base);
87
+ }
88
+
89
+ /**
90
+ * Reset in-memory state. Called when auto-mode stops.
91
+ */
92
+ export function resetMetrics(): void {
93
+ ledger = null;
94
+ basePath = "";
95
+ }
96
+
97
+ /**
98
+ * Snapshot usage metrics from the current session before it's wiped.
99
+ * Scans session entries for AssistantMessage usage data.
100
+ */
101
+ export function snapshotUnitMetrics(
102
+ ctx: ExtensionContext,
103
+ unitType: string,
104
+ unitId: string,
105
+ startedAt: number,
106
+ model: string,
107
+ ): UnitMetrics | null {
108
+ if (!ledger) return null;
109
+
110
+ const entries = ctx.sessionManager.getEntries();
111
+ if (!entries || entries.length === 0) return null;
112
+
113
+ const tokens: TokenCounts = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
114
+ let cost = 0;
115
+ let toolCalls = 0;
116
+ let assistantMessages = 0;
117
+ let userMessages = 0;
118
+
119
+ for (const entry of entries) {
120
+ if (entry.type !== "message") continue;
121
+ const msg = (entry as any).message;
122
+ if (!msg) continue;
123
+
124
+ if (msg.role === "assistant") {
125
+ assistantMessages++;
126
+ if (msg.usage) {
127
+ tokens.input += msg.usage.input ?? 0;
128
+ tokens.output += msg.usage.output ?? 0;
129
+ tokens.cacheRead += msg.usage.cacheRead ?? 0;
130
+ tokens.cacheWrite += msg.usage.cacheWrite ?? 0;
131
+ tokens.total += msg.usage.totalTokens ?? 0;
132
+ if (msg.usage.cost != null) {
133
+ const c = msg.usage.cost;
134
+ cost += typeof c === "number" ? c : (c.total ?? 0);
135
+ }
136
+ }
137
+ // Count tool calls in this message
138
+ if (msg.content && Array.isArray(msg.content)) {
139
+ for (const block of msg.content) {
140
+ if (block.type === "tool_call") toolCalls++;
141
+ }
142
+ }
143
+ } else if (msg.role === "user") {
144
+ userMessages++;
145
+ }
146
+ }
147
+
148
+ const unit: UnitMetrics = {
149
+ type: unitType,
150
+ id: unitId,
151
+ model,
152
+ startedAt,
153
+ finishedAt: Date.now(),
154
+ tokens,
155
+ cost,
156
+ toolCalls,
157
+ assistantMessages,
158
+ userMessages,
159
+ };
160
+
161
+ ledger.units.push(unit);
162
+ saveLedger(basePath, ledger);
163
+
164
+ return unit;
165
+ }
166
+
167
+ /**
168
+ * Get the current ledger (read-only).
169
+ */
170
+ export function getLedger(): MetricsLedger | null {
171
+ return ledger;
172
+ }
173
+
174
+ // ─── Aggregation helpers ──────────────────────────────────────────────────────
175
+
176
+ export interface PhaseAggregate {
177
+ phase: MetricsPhase;
178
+ units: number;
179
+ tokens: TokenCounts;
180
+ cost: number;
181
+ duration: number; // ms
182
+ }
183
+
184
+ export interface SliceAggregate {
185
+ sliceId: string;
186
+ units: number;
187
+ tokens: TokenCounts;
188
+ cost: number;
189
+ duration: number;
190
+ }
191
+
192
+ export interface ModelAggregate {
193
+ model: string;
194
+ units: number;
195
+ tokens: TokenCounts;
196
+ cost: number;
197
+ }
198
+
199
+ export interface ProjectTotals {
200
+ units: number;
201
+ tokens: TokenCounts;
202
+ cost: number;
203
+ duration: number;
204
+ toolCalls: number;
205
+ assistantMessages: number;
206
+ userMessages: number;
207
+ }
208
+
209
+ function emptyTokens(): TokenCounts {
210
+ return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 };
211
+ }
212
+
213
+ function addTokens(a: TokenCounts, b: TokenCounts): TokenCounts {
214
+ return {
215
+ input: a.input + b.input,
216
+ output: a.output + b.output,
217
+ cacheRead: a.cacheRead + b.cacheRead,
218
+ cacheWrite: a.cacheWrite + b.cacheWrite,
219
+ total: a.total + b.total,
220
+ };
221
+ }
222
+
223
+ export function aggregateByPhase(units: UnitMetrics[]): PhaseAggregate[] {
224
+ const map = new Map<MetricsPhase, PhaseAggregate>();
225
+ for (const u of units) {
226
+ const phase = classifyUnitPhase(u.type);
227
+ let agg = map.get(phase);
228
+ if (!agg) {
229
+ agg = { phase, units: 0, tokens: emptyTokens(), cost: 0, duration: 0 };
230
+ map.set(phase, agg);
231
+ }
232
+ agg.units++;
233
+ agg.tokens = addTokens(agg.tokens, u.tokens);
234
+ agg.cost += u.cost;
235
+ agg.duration += u.finishedAt - u.startedAt;
236
+ }
237
+ // Return in a stable order
238
+ const order: MetricsPhase[] = ["research", "planning", "execution", "completion", "reassessment"];
239
+ return order.map(p => map.get(p)).filter((a): a is PhaseAggregate => !!a);
240
+ }
241
+
242
+ export function aggregateBySlice(units: UnitMetrics[]): SliceAggregate[] {
243
+ const map = new Map<string, SliceAggregate>();
244
+ for (const u of units) {
245
+ const parts = u.id.split("/");
246
+ // Slice ID is parts[0]/parts[1] if it exists, else parts[0]
247
+ const sliceId = parts.length >= 2 ? `${parts[0]}/${parts[1]}` : parts[0];
248
+ let agg = map.get(sliceId);
249
+ if (!agg) {
250
+ agg = { sliceId, units: 0, tokens: emptyTokens(), cost: 0, duration: 0 };
251
+ map.set(sliceId, agg);
252
+ }
253
+ agg.units++;
254
+ agg.tokens = addTokens(agg.tokens, u.tokens);
255
+ agg.cost += u.cost;
256
+ agg.duration += u.finishedAt - u.startedAt;
257
+ }
258
+ return Array.from(map.values()).sort((a, b) => a.sliceId.localeCompare(b.sliceId));
259
+ }
260
+
261
+ export function aggregateByModel(units: UnitMetrics[]): ModelAggregate[] {
262
+ const map = new Map<string, ModelAggregate>();
263
+ for (const u of units) {
264
+ let agg = map.get(u.model);
265
+ if (!agg) {
266
+ agg = { model: u.model, units: 0, tokens: emptyTokens(), cost: 0 };
267
+ map.set(u.model, agg);
268
+ }
269
+ agg.units++;
270
+ agg.tokens = addTokens(agg.tokens, u.tokens);
271
+ agg.cost += u.cost;
272
+ }
273
+ return Array.from(map.values()).sort((a, b) => b.cost - a.cost);
274
+ }
275
+
276
+ export function getProjectTotals(units: UnitMetrics[]): ProjectTotals {
277
+ const totals: ProjectTotals = {
278
+ units: units.length,
279
+ tokens: emptyTokens(),
280
+ cost: 0,
281
+ duration: 0,
282
+ toolCalls: 0,
283
+ assistantMessages: 0,
284
+ userMessages: 0,
285
+ };
286
+ for (const u of units) {
287
+ totals.tokens = addTokens(totals.tokens, u.tokens);
288
+ totals.cost += u.cost;
289
+ totals.duration += u.finishedAt - u.startedAt;
290
+ totals.toolCalls += u.toolCalls;
291
+ totals.assistantMessages += u.assistantMessages;
292
+ totals.userMessages += u.userMessages;
293
+ }
294
+ return totals;
295
+ }
296
+
297
+ // ─── Formatting helpers ───────────────────────────────────────────────────────
298
+
299
+ export function formatCost(cost: number): string {
300
+ const n = Number(cost) || 0;
301
+ if (n < 0.01) return `$${n.toFixed(4)}`;
302
+ if (n < 1) return `$${n.toFixed(3)}`;
303
+ return `$${n.toFixed(2)}`;
304
+ }
305
+
306
+ /**
307
+ * Compute a projected remaining cost based on completed slice averages.
308
+ *
309
+ * Filters to slice-level entries (sliceId contains "/") to exclude bare milestone
310
+ * aggregates from the average. Returns [] when fewer than 2 slice-level entries
311
+ * exist (insufficient data for a reliable projection).
312
+ *
313
+ * If `budgetCeiling` is provided and `totalCost >= budgetCeiling`, a warning line
314
+ * is appended to the result.
315
+ */
316
+ export function formatCostProjection(
317
+ completedSlices: SliceAggregate[],
318
+ remainingCount: number,
319
+ budgetCeiling?: number,
320
+ ): string[] {
321
+ const sliceLevel = completedSlices.filter(s => s.sliceId.includes("/"));
322
+ if (sliceLevel.length < 2) return [];
323
+
324
+ const totalCost = sliceLevel.reduce((sum, s) => sum + s.cost, 0);
325
+ const avgCost = totalCost / sliceLevel.length;
326
+ const projected = avgCost * remainingCount;
327
+
328
+ const projLine = `Projected remaining: ${formatCost(projected)} (${formatCost(avgCost)}/slice avg × ${remainingCount} remaining)`;
329
+ const result: string[] = [projLine];
330
+
331
+ if (budgetCeiling !== undefined && totalCost >= budgetCeiling) {
332
+ result.push(`Budget ceiling ${formatCost(budgetCeiling)} reached (spent ${formatCost(totalCost)})`);
333
+ }
334
+
335
+ return result;
336
+ }
337
+
338
+ export function formatTokenCount(count: number): string {
339
+ if (count < 1000) return `${count}`;
340
+ if (count < 1_000_000) return `${(count / 1000).toFixed(1)}k`;
341
+ return `${(count / 1_000_000).toFixed(2)}M`;
342
+ }
343
+
344
+ // ─── Disk I/O ─────────────────────────────────────────────────────────────────
345
+
346
+ function metricsPath(base: string): string {
347
+ return join(gsdRoot(base), "metrics.json");
348
+ }
349
+
350
+ function loadLedger(base: string): MetricsLedger {
351
+ try {
352
+ const raw = readFileSync(metricsPath(base), "utf-8");
353
+ const parsed = JSON.parse(raw);
354
+ if (parsed.version === 1 && Array.isArray(parsed.units)) {
355
+ return parsed as MetricsLedger;
356
+ }
357
+ } catch {
358
+ // File doesn't exist or is corrupt — start fresh
359
+ }
360
+ return {
361
+ version: 1,
362
+ projectStartedAt: Date.now(),
363
+ units: [],
364
+ };
365
+ }
366
+
367
+ function saveLedger(base: string, data: MetricsLedger): void {
368
+ try {
369
+ mkdirSync(gsdRoot(base), { recursive: true });
370
+ writeFileSync(metricsPath(base), JSON.stringify(data, null, 2) + "\n", "utf-8");
371
+ } catch {
372
+ // Don't let metrics failures break auto-mode
373
+ }
374
+ }
@@ -0,0 +1,218 @@
1
+ /**
2
+ * /gsd migrate — one-shot migration from .planning to .gsd
3
+ *
4
+ * Thin UX orchestrator: resolves paths, runs the validate → parse → transform →
5
+ * preview → write pipeline, and shows confirmation UI via showNextAction.
6
+ * All business logic lives in the pipeline modules (S01–S03).
7
+ *
8
+ * After a successful write, offers an agent-driven review that audits the
9
+ * output for GSD-2 standards compliance.
10
+ */
11
+
12
+ import type { ExtensionAPI, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
13
+ import { existsSync, readFileSync } from "node:fs";
14
+ import { resolve, join, dirname } from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+ import { showNextAction } from "../../shared/next-action-ui.js";
17
+ import {
18
+ validatePlanningDirectory,
19
+ parsePlanningDirectory,
20
+ transformToGSD,
21
+ generatePreview,
22
+ writeGSDDirectory,
23
+ } from "./index.js";
24
+
25
+ import type { MigrationPreview } from "./writer.js";
26
+
27
+ /** Format preview stats for embedding in the review prompt. */
28
+ function formatPreviewStats(preview: MigrationPreview): string {
29
+ const lines = [
30
+ `- Milestones: ${preview.milestoneCount}`,
31
+ `- Slices: ${preview.totalSlices} (${preview.doneSlices} done — ${preview.sliceCompletionPct}%)`,
32
+ `- Tasks: ${preview.totalTasks} (${preview.doneTasks} done — ${preview.taskCompletionPct}%)`,
33
+ ];
34
+ if (preview.requirements.total > 0) {
35
+ lines.push(
36
+ `- Requirements: ${preview.requirements.total} (${preview.requirements.validated} validated, ${preview.requirements.active} active, ${preview.requirements.deferred} deferred)`,
37
+ );
38
+ }
39
+ return lines.join("\n");
40
+ }
41
+
42
+ /** Load and interpolate the review-migration prompt template. */
43
+ function buildReviewPrompt(
44
+ sourcePath: string,
45
+ gsdPath: string,
46
+ preview: MigrationPreview,
47
+ ): string {
48
+ const promptsDir = join(dirname(fileURLToPath(import.meta.url)), "..", "prompts");
49
+ const templatePath = join(promptsDir, "review-migration.md");
50
+ let content = readFileSync(templatePath, "utf-8");
51
+
52
+ content = content.replaceAll("{{sourcePath}}", sourcePath);
53
+ content = content.replaceAll("{{gsdPath}}", gsdPath);
54
+ content = content.replaceAll("{{previewStats}}", formatPreviewStats(preview));
55
+
56
+ return content.trim();
57
+ }
58
+
59
+ /** Dispatch the review prompt to the agent. */
60
+ function dispatchReview(
61
+ pi: ExtensionAPI,
62
+ sourcePath: string,
63
+ gsdPath: string,
64
+ preview: MigrationPreview,
65
+ ): void {
66
+ const prompt = buildReviewPrompt(sourcePath, gsdPath, preview);
67
+
68
+ pi.sendMessage(
69
+ {
70
+ customType: "gsd-migrate-review",
71
+ content: prompt,
72
+ display: false,
73
+ },
74
+ { triggerTurn: true },
75
+ );
76
+ }
77
+
78
+ export async function handleMigrate(
79
+ args: string,
80
+ ctx: ExtensionCommandContext,
81
+ pi: ExtensionAPI,
82
+ ): Promise<void> {
83
+ // ── Resolve source path ────────────────────────────────────────────────────
84
+ // Default to cwd when no args given; expand ~ to HOME
85
+ let rawPath = args.trim() || ".";
86
+ if (rawPath.startsWith("~/")) {
87
+ rawPath = join(process.env.HOME ?? "~", rawPath.slice(2));
88
+ } else if (rawPath === "~") {
89
+ rawPath = process.env.HOME ?? "~";
90
+ }
91
+
92
+ let sourcePath = resolve(process.cwd(), rawPath);
93
+ if (!sourcePath.endsWith(".planning")) {
94
+ sourcePath = join(sourcePath, ".planning");
95
+ }
96
+
97
+ if (!existsSync(sourcePath)) {
98
+ ctx.ui.notify(
99
+ `Directory not found: ${sourcePath}\n\n` +
100
+ 'Migration converts a .planning/ directory (from older GSD versions) into .gsd/ format.\n' +
101
+ 'If you are starting a new project, use /gsd:new-project instead.\n' +
102
+ 'If migrating, ensure the path contains a .planning/ directory.',
103
+ "error",
104
+ );
105
+ return;
106
+ }
107
+
108
+ // ── Validate ───────────────────────────────────────────────────────────────
109
+ const validation = await validatePlanningDirectory(sourcePath);
110
+
111
+ const warnings = validation.issues.filter((i) => i.severity === "warning");
112
+ const fatals = validation.issues.filter((i) => i.severity === "fatal");
113
+
114
+ for (const w of warnings) {
115
+ ctx.ui.notify(`⚠ ${w.message} (${w.file})`, "warning");
116
+ }
117
+ for (const f of fatals) {
118
+ ctx.ui.notify(`✖ ${f.message} (${f.file})`, "error");
119
+ }
120
+
121
+ if (!validation.valid) {
122
+ ctx.ui.notify(
123
+ "Migration blocked — fix the fatal issues above before retrying.",
124
+ "error",
125
+ );
126
+ return;
127
+ }
128
+
129
+ // ── Parse → Transform → Preview ───────────────────────────────────────────
130
+ const parsed = await parsePlanningDirectory(sourcePath);
131
+ const project = transformToGSD(parsed);
132
+ const preview = generatePreview(project);
133
+
134
+ // ── Build preview text ─────────────────────────────────────────────────────
135
+ const lines: string[] = [
136
+ `Milestones: ${preview.milestoneCount}`,
137
+ `Slices: ${preview.totalSlices} (${preview.doneSlices} done — ${preview.sliceCompletionPct}%)`,
138
+ `Tasks: ${preview.totalTasks} (${preview.doneTasks} done — ${preview.taskCompletionPct}%)`,
139
+ ];
140
+
141
+ if (preview.requirements.total > 0) {
142
+ lines.push(
143
+ `Requirements: ${preview.requirements.total} (${preview.requirements.validated} validated, ${preview.requirements.active} active, ${preview.requirements.deferred} deferred)`,
144
+ );
145
+ }
146
+
147
+ const targetGsdExists = existsSync(join(process.cwd(), ".gsd"));
148
+ if (targetGsdExists) {
149
+ lines.push("");
150
+ lines.push("⚠ A .gsd directory already exists in the current working directory — it will be overwritten.");
151
+ }
152
+
153
+ // ── Confirmation via showNextAction ────────────────────────────────────────
154
+ const choice = await showNextAction(ctx as any, {
155
+ title: "Migration preview",
156
+ summary: lines,
157
+ actions: [
158
+ {
159
+ id: "confirm",
160
+ label: "Write .gsd directory",
161
+ description: `Migrate ${preview.milestoneCount} milestone(s) to ${process.cwd()}/.gsd`,
162
+ recommended: true,
163
+ },
164
+ {
165
+ id: "cancel",
166
+ label: "Cancel",
167
+ description: "Exit without writing anything",
168
+ },
169
+ ],
170
+ notYetMessage: "Run /gsd migrate again when ready.",
171
+ });
172
+
173
+ if (choice !== "confirm") {
174
+ ctx.ui.notify("Migration cancelled — no files were written.", "info");
175
+ return;
176
+ }
177
+
178
+ // ── Write ──────────────────────────────────────────────────────────────────
179
+ ctx.ui.notify("Writing .gsd directory…", "info");
180
+
181
+ const result = await writeGSDDirectory(project, process.cwd());
182
+ const gsdPath = join(process.cwd(), ".gsd");
183
+
184
+ ctx.ui.notify(
185
+ `✓ Migration complete — ${result.paths.length} file(s) written to .gsd/`,
186
+ "info",
187
+ );
188
+
189
+ // ── Post-write review offer ────────────────────────────────────────────────
190
+ const reviewChoice = await showNextAction(ctx as any, {
191
+ title: "Migration written",
192
+ summary: [
193
+ `${result.paths.length} files written to .gsd/`,
194
+ "",
195
+ "The agent can now review the migrated output against GSD-2 standards —",
196
+ "checking structure, content quality, deriveState() round-trip, and",
197
+ "requirement statuses. It will fix minor issues in-place.",
198
+ ],
199
+ actions: [
200
+ {
201
+ id: "review",
202
+ label: "Review migration",
203
+ description: "Agent audits the .gsd output and reports PASS/FAIL per category",
204
+ recommended: true,
205
+ },
206
+ {
207
+ id: "skip",
208
+ label: "Skip review",
209
+ description: "Trust the migration output as-is",
210
+ },
211
+ ],
212
+ notYetMessage: "Run /gsd migrate again to re-migrate, or review .gsd manually.",
213
+ });
214
+
215
+ if (reviewChoice === "review") {
216
+ dispatchReview(pi, sourcePath, gsdPath, preview);
217
+ }
218
+ }
@@ -0,0 +1,42 @@
1
+ // Barrel export for old .planning migration module
2
+
3
+ export { handleMigrate } from './command.ts';
4
+ export { parsePlanningDirectory } from './parser.ts';
5
+ export { validatePlanningDirectory } from './validator.ts';
6
+ export { transformToGSD } from './transformer.ts';
7
+ export { writeGSDDirectory } from './writer.ts';
8
+ export type { WrittenFiles, MigrationPreview } from './writer.ts';
9
+ export { generatePreview } from './preview.ts';
10
+ export type {
11
+ // Input types (old .planning format)
12
+ PlanningProject,
13
+ PlanningPhase,
14
+ PlanningPlan,
15
+ PlanningPlanFrontmatter,
16
+ PlanningPlanMustHaves,
17
+ PlanningSummary,
18
+ PlanningSummaryFrontmatter,
19
+ PlanningSummaryRequires,
20
+ PlanningRoadmap,
21
+ PlanningRoadmapMilestone,
22
+ PlanningRoadmapEntry,
23
+ PlanningRequirement,
24
+ PlanningResearch,
25
+ PlanningConfig,
26
+ PlanningQuickTask,
27
+ PlanningMilestone,
28
+ PlanningState,
29
+ PlanningPhaseFile,
30
+ ValidationResult,
31
+ ValidationIssue,
32
+ ValidationSeverity,
33
+ // Output types (GSD-2 format)
34
+ GSDProject,
35
+ GSDMilestone,
36
+ GSDSlice,
37
+ GSDTask,
38
+ GSDRequirement,
39
+ GSDSliceSummaryData,
40
+ GSDTaskSummaryData,
41
+ GSDBoundaryEntry,
42
+ } from './types.ts';