@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,308 @@
1
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import {
5
+ resolveExpectedArtifactPath,
6
+ writeBlockerPlaceholder,
7
+ skipExecuteTask,
8
+ } from "../auto.ts";
9
+
10
+ let passed = 0;
11
+ let failed = 0;
12
+
13
+ function assert(condition: boolean, message: string): void {
14
+ if (condition) passed++;
15
+ else {
16
+ failed++;
17
+ console.error(` FAIL: ${message}`);
18
+ }
19
+ }
20
+
21
+ function assertEq<T>(actual: T, expected: T, message: string): void {
22
+ if (JSON.stringify(actual) === JSON.stringify(expected)) passed++;
23
+ else {
24
+ failed++;
25
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
26
+ }
27
+ }
28
+
29
+ function createFixtureBase(): string {
30
+ const base = mkdtempSync(join(tmpdir(), "gsd-idle-recovery-test-"));
31
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
32
+ return base;
33
+ }
34
+
35
+ function cleanup(base: string): void {
36
+ rmSync(base, { recursive: true, force: true });
37
+ }
38
+
39
+ // ═══ resolveExpectedArtifactPath ═════════════════════════════════════════════
40
+
41
+ {
42
+ console.log("\n=== resolveExpectedArtifactPath: research-milestone ===");
43
+ const base = createFixtureBase();
44
+ try {
45
+ const result = resolveExpectedArtifactPath("research-milestone", "M001", base);
46
+ assert(result !== null, "should resolve a path");
47
+ assert(result!.endsWith("M001-RESEARCH.md"), `path should end with M001-RESEARCH.md, got ${result}`);
48
+ } finally {
49
+ cleanup(base);
50
+ }
51
+ }
52
+
53
+ {
54
+ console.log("\n=== resolveExpectedArtifactPath: plan-milestone ===");
55
+ const base = createFixtureBase();
56
+ try {
57
+ const result = resolveExpectedArtifactPath("plan-milestone", "M001", base);
58
+ assert(result !== null, "should resolve a path");
59
+ assert(result!.endsWith("M001-ROADMAP.md"), `path should end with M001-ROADMAP.md, got ${result}`);
60
+ } finally {
61
+ cleanup(base);
62
+ }
63
+ }
64
+
65
+ {
66
+ console.log("\n=== resolveExpectedArtifactPath: research-slice ===");
67
+ const base = createFixtureBase();
68
+ try {
69
+ const result = resolveExpectedArtifactPath("research-slice", "M001/S01", base);
70
+ assert(result !== null, "should resolve a path");
71
+ assert(result!.endsWith("S01-RESEARCH.md"), `path should end with S01-RESEARCH.md, got ${result}`);
72
+ } finally {
73
+ cleanup(base);
74
+ }
75
+ }
76
+
77
+ {
78
+ console.log("\n=== resolveExpectedArtifactPath: plan-slice ===");
79
+ const base = createFixtureBase();
80
+ try {
81
+ const result = resolveExpectedArtifactPath("plan-slice", "M001/S01", base);
82
+ assert(result !== null, "should resolve a path");
83
+ assert(result!.endsWith("S01-PLAN.md"), `path should end with S01-PLAN.md, got ${result}`);
84
+ } finally {
85
+ cleanup(base);
86
+ }
87
+ }
88
+
89
+ {
90
+ console.log("\n=== resolveExpectedArtifactPath: complete-milestone ===");
91
+ const base = createFixtureBase();
92
+ try {
93
+ const result = resolveExpectedArtifactPath("complete-milestone", "M001", base);
94
+ assert(result !== null, "should resolve a path");
95
+ assert(result!.endsWith("M001-SUMMARY.md"), `path should end with M001-SUMMARY.md, got ${result}`);
96
+ } finally {
97
+ cleanup(base);
98
+ }
99
+ }
100
+
101
+ {
102
+ console.log("\n=== resolveExpectedArtifactPath: unknown unit type → null ===");
103
+ const base = createFixtureBase();
104
+ try {
105
+ const result = resolveExpectedArtifactPath("unknown-type", "M001/S01", base);
106
+ assertEq(result, null, "unknown type returns null");
107
+ } finally {
108
+ cleanup(base);
109
+ }
110
+ }
111
+
112
+ // ═══ writeBlockerPlaceholder ═════════════════════════════════════════════════
113
+
114
+ {
115
+ console.log("\n=== writeBlockerPlaceholder: writes file for research-slice ===");
116
+ const base = createFixtureBase();
117
+ try {
118
+ const result = writeBlockerPlaceholder("research-slice", "M001/S01", base, "idle recovery exhausted 2 attempts");
119
+ assert(result !== null, "should return relative path");
120
+ const absPath = resolveExpectedArtifactPath("research-slice", "M001/S01", base)!;
121
+ assert(existsSync(absPath), "file should exist on disk");
122
+ const content = readFileSync(absPath, "utf-8");
123
+ assert(content.includes("BLOCKER"), "should contain BLOCKER heading");
124
+ assert(content.includes("idle recovery exhausted 2 attempts"), "should contain the reason");
125
+ assert(content.includes("research-slice"), "should mention the unit type");
126
+ assert(content.includes("M001/S01"), "should mention the unit ID");
127
+ } finally {
128
+ cleanup(base);
129
+ }
130
+ }
131
+
132
+ {
133
+ console.log("\n=== writeBlockerPlaceholder: creates directory if missing ===");
134
+ const base = mkdtempSync(join(tmpdir(), "gsd-idle-recovery-test-"));
135
+ try {
136
+ // Only create milestone dir, not slice dir
137
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
138
+ // resolveSlicePath needs the slice dir to exist to resolve, so this should return null
139
+ const result = writeBlockerPlaceholder("research-slice", "M001/S01", base, "test reason");
140
+ // Since the slice dir doesn't exist, resolveExpectedArtifactPath returns null
141
+ assertEq(result, null, "returns null when directory structure doesn't exist");
142
+ } finally {
143
+ cleanup(base);
144
+ }
145
+ }
146
+
147
+ {
148
+ console.log("\n=== writeBlockerPlaceholder: writes file for research-milestone ===");
149
+ const base = createFixtureBase();
150
+ try {
151
+ const result = writeBlockerPlaceholder("research-milestone", "M001", base, "hard timeout");
152
+ assert(result !== null, "should return relative path");
153
+ const absPath = resolveExpectedArtifactPath("research-milestone", "M001", base)!;
154
+ assert(existsSync(absPath), "file should exist on disk");
155
+ const content = readFileSync(absPath, "utf-8");
156
+ assert(content.includes("BLOCKER"), "should contain BLOCKER heading");
157
+ assert(content.includes("hard timeout"), "should contain the reason");
158
+ } finally {
159
+ cleanup(base);
160
+ }
161
+ }
162
+
163
+ {
164
+ console.log("\n=== writeBlockerPlaceholder: unknown type → null ===");
165
+ const base = createFixtureBase();
166
+ try {
167
+ const result = writeBlockerPlaceholder("execute-task", "M001/S01/T01", base, "test");
168
+ assertEq(result, null, "execute-task has no single artifact path, returns null");
169
+ } finally {
170
+ cleanup(base);
171
+ }
172
+ }
173
+
174
+ // ═══ skipExecuteTask ═════════════════════════════════════════════════════════
175
+
176
+ {
177
+ console.log("\n=== skipExecuteTask: writes summary and checks plan checkbox ===");
178
+ const base = createFixtureBase();
179
+ try {
180
+ const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
181
+ writeFileSync(planPath, [
182
+ "# S01: Test Slice",
183
+ "",
184
+ "## Tasks",
185
+ "",
186
+ "- [ ] **T01: First task** `est:10m`",
187
+ " Do the first thing.",
188
+ "- [ ] **T02: Second task** `est:15m`",
189
+ " Do the second thing.",
190
+ ].join("\n"), "utf-8");
191
+
192
+ const result = skipExecuteTask(
193
+ base, "M001", "S01", "T01",
194
+ { summaryExists: false, taskChecked: false },
195
+ "idle", 2,
196
+ );
197
+
198
+ assert(result === true, "should return true");
199
+
200
+ // Check summary was written
201
+ const summaryPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md");
202
+ assert(existsSync(summaryPath), "task summary should exist");
203
+ const summaryContent = readFileSync(summaryPath, "utf-8");
204
+ assert(summaryContent.includes("BLOCKER"), "summary should contain BLOCKER");
205
+ assert(summaryContent.includes("T01"), "summary should mention task ID");
206
+
207
+ // Check plan checkbox was marked
208
+ const planContent = readFileSync(planPath, "utf-8");
209
+ assert(planContent.includes("- [x] **T01:"), "T01 should be checked");
210
+ assert(planContent.includes("- [ ] **T02:"), "T02 should remain unchecked");
211
+ } finally {
212
+ cleanup(base);
213
+ }
214
+ }
215
+
216
+ {
217
+ console.log("\n=== skipExecuteTask: skips summary if already exists ===");
218
+ const base = createFixtureBase();
219
+ try {
220
+ const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
221
+ writeFileSync(planPath, "- [ ] **T01: Task** `est:10m`\n", "utf-8");
222
+
223
+ // Pre-write a summary
224
+ const summaryPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md");
225
+ writeFileSync(summaryPath, "# Real summary\nActual work done.", "utf-8");
226
+
227
+ const result = skipExecuteTask(
228
+ base, "M001", "S01", "T01",
229
+ { summaryExists: true, taskChecked: false },
230
+ "idle", 2,
231
+ );
232
+
233
+ assert(result === true, "should return true");
234
+
235
+ // Summary should be untouched (not overwritten with blocker)
236
+ const content = readFileSync(summaryPath, "utf-8");
237
+ assert(content.includes("Real summary"), "original summary should be preserved");
238
+ assert(!content.includes("BLOCKER"), "should not contain BLOCKER");
239
+
240
+ // Plan checkbox should still be marked
241
+ const planContent = readFileSync(planPath, "utf-8");
242
+ assert(planContent.includes("- [x] **T01:"), "T01 should be checked");
243
+ } finally {
244
+ cleanup(base);
245
+ }
246
+ }
247
+
248
+ {
249
+ console.log("\n=== skipExecuteTask: skips checkbox if already checked ===");
250
+ const base = createFixtureBase();
251
+ try {
252
+ const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
253
+ writeFileSync(planPath, "- [x] **T01: Task** `est:10m`\n", "utf-8");
254
+
255
+ const result = skipExecuteTask(
256
+ base, "M001", "S01", "T01",
257
+ { summaryExists: false, taskChecked: true },
258
+ "idle", 2,
259
+ );
260
+
261
+ assert(result === true, "should return true");
262
+
263
+ // Summary should be written (since summaryExists was false)
264
+ const summaryPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md");
265
+ assert(existsSync(summaryPath), "task summary should exist");
266
+
267
+ // Plan checkbox should be untouched
268
+ const planContent = readFileSync(planPath, "utf-8");
269
+ assert(planContent.includes("- [x] **T01:"), "T01 should remain checked");
270
+ } finally {
271
+ cleanup(base);
272
+ }
273
+ }
274
+
275
+ {
276
+ console.log("\n=== skipExecuteTask: handles special regex chars in task ID ===");
277
+ const base = createFixtureBase();
278
+ try {
279
+ const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
280
+ writeFileSync(planPath, "- [ ] **T01.1: Sub-task** `est:10m`\n", "utf-8");
281
+
282
+ const result = skipExecuteTask(
283
+ base, "M001", "S01", "T01.1",
284
+ { summaryExists: false, taskChecked: false },
285
+ "idle", 2,
286
+ );
287
+
288
+ assert(result === true, "should return true");
289
+
290
+ const planContent = readFileSync(planPath, "utf-8");
291
+ assert(planContent.includes("- [x] **T01.1:"), "T01.1 should be checked (regex chars escaped)");
292
+ } finally {
293
+ cleanup(base);
294
+ }
295
+ }
296
+
297
+ // ═════════════════════════════════════════════════════════════════════════════
298
+ // Results
299
+ // ═════════════════════════════════════════════════════════════════════════════
300
+
301
+ console.log(`\n${"=".repeat(40)}`);
302
+ if (failed > 0) {
303
+ console.log(`Results: ${passed} passed, ${failed} failed`);
304
+ process.exit(1);
305
+ } else {
306
+ console.log(`Results: ${passed} passed, ${failed} failed`);
307
+ console.log("All tests passed ✓");
308
+ }
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Tests for GSD metrics disk I/O — init, snapshot, load/save cycle.
3
+ * Uses a temp directory to avoid touching real .gsd/ state.
4
+ */
5
+
6
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { tmpdir } from "node:os";
9
+ import {
10
+ initMetrics,
11
+ resetMetrics,
12
+ getLedger,
13
+ snapshotUnitMetrics,
14
+ type MetricsLedger,
15
+ } from "../metrics.js";
16
+
17
+ let passed = 0;
18
+ let failed = 0;
19
+
20
+ function assert(condition: boolean, message: string): void {
21
+ if (condition) {
22
+ passed++;
23
+ } else {
24
+ failed++;
25
+ console.error(` FAIL: ${message}`);
26
+ }
27
+ }
28
+
29
+ function assertEq<T>(actual: T, expected: T, message: string): void {
30
+ if (JSON.stringify(actual) === JSON.stringify(expected)) {
31
+ passed++;
32
+ } else {
33
+ failed++;
34
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
35
+ }
36
+ }
37
+
38
+ // ─── Setup ────────────────────────────────────────────────────────────────────
39
+
40
+ const tmpBase = mkdtempSync(join(tmpdir(), "gsd-metrics-test-"));
41
+ mkdirSync(join(tmpBase, ".gsd"), { recursive: true });
42
+
43
+ // Mock ExtensionContext with session entries
44
+ function mockCtx(messages: any[] = []): any {
45
+ const entries = messages.map((msg, i) => ({
46
+ type: "message",
47
+ id: `entry-${i}`,
48
+ parentId: i > 0 ? `entry-${i - 1}` : null,
49
+ timestamp: new Date().toISOString(),
50
+ message: msg,
51
+ }));
52
+ return {
53
+ sessionManager: {
54
+ getEntries: () => entries,
55
+ },
56
+ model: { id: "claude-sonnet-4-20250514" },
57
+ };
58
+ }
59
+
60
+ // ─── Tests ────────────────────────────────────────────────────────────────────
61
+
62
+ console.log("\n=== initMetrics / getLedger ===");
63
+
64
+ {
65
+ resetMetrics();
66
+ assert(getLedger() === null, "ledger null before init");
67
+
68
+ initMetrics(tmpBase);
69
+ const ledger = getLedger();
70
+ assert(ledger !== null, "ledger not null after init");
71
+ assertEq(ledger!.version, 1, "version is 1");
72
+ assertEq(ledger!.units.length, 0, "no units initially");
73
+ }
74
+
75
+ console.log("\n=== snapshotUnitMetrics ===");
76
+
77
+ {
78
+ resetMetrics();
79
+ initMetrics(tmpBase);
80
+
81
+ // Simulate a session with assistant messages containing usage data
82
+ const ctx = mockCtx([
83
+ { role: "user", content: "Do the thing" },
84
+ {
85
+ role: "assistant",
86
+ content: [
87
+ { type: "text", text: "I'll do the thing" },
88
+ { type: "tool_call", id: "tc1", name: "bash", input: {} },
89
+ ],
90
+ usage: {
91
+ input: 5000,
92
+ output: 2000,
93
+ cacheRead: 3000,
94
+ cacheWrite: 500,
95
+ totalTokens: 10500,
96
+ cost: { input: 0.015, output: 0.03, cacheRead: 0.003, cacheWrite: 0.002, total: 0.05 },
97
+ },
98
+ },
99
+ { role: "toolResult", toolCallId: "tc1", content: [{ type: "text", text: "ok" }] },
100
+ {
101
+ role: "assistant",
102
+ content: [{ type: "text", text: "Done!" }],
103
+ usage: {
104
+ input: 8000,
105
+ output: 1000,
106
+ cacheRead: 6000,
107
+ cacheWrite: 200,
108
+ totalTokens: 15200,
109
+ cost: { input: 0.024, output: 0.015, cacheRead: 0.006, cacheWrite: 0.001, total: 0.046 },
110
+ },
111
+ },
112
+ ]);
113
+
114
+ const unit = snapshotUnitMetrics(ctx, "execute-task", "M001/S01/T01", Date.now() - 5000, "claude-sonnet-4-20250514");
115
+
116
+ assert(unit !== null, "unit returned");
117
+ assertEq(unit!.type, "execute-task", "type");
118
+ assertEq(unit!.id, "M001/S01/T01", "id");
119
+ assertEq(unit!.tokens.input, 13000, "input tokens (5000+8000)");
120
+ assertEq(unit!.tokens.output, 3000, "output tokens (2000+1000)");
121
+ assertEq(unit!.tokens.cacheRead, 9000, "cacheRead (3000+6000)");
122
+ assertEq(unit!.tokens.total, 25700, "total tokens (10500+15200)");
123
+ assert(Math.abs(unit!.cost - 0.096) < 0.001, `cost ~0.096 (got ${unit!.cost})`);
124
+ assertEq(unit!.toolCalls, 1, "1 tool call");
125
+ assertEq(unit!.assistantMessages, 2, "2 assistant messages");
126
+ assertEq(unit!.userMessages, 1, "1 user message");
127
+
128
+ // Verify ledger persisted
129
+ const ledger = getLedger()!;
130
+ assertEq(ledger.units.length, 1, "1 unit in ledger");
131
+ }
132
+
133
+ console.log("\n=== Persistence across init/reset cycles ===");
134
+
135
+ {
136
+ // Reset and re-init — should load from disk
137
+ resetMetrics();
138
+ initMetrics(tmpBase);
139
+
140
+ const ledger = getLedger()!;
141
+ assertEq(ledger.units.length, 1, "unit survived reset+init");
142
+ assertEq(ledger.units[0].id, "M001/S01/T01", "correct unit ID");
143
+
144
+ // Add another unit
145
+ const ctx = mockCtx([
146
+ {
147
+ role: "assistant",
148
+ content: [{ type: "text", text: "Research complete" }],
149
+ usage: {
150
+ input: 3000, output: 1500, cacheRead: 1000, cacheWrite: 300, totalTokens: 5800,
151
+ cost: { input: 0.009, output: 0.023, cacheRead: 0.001, cacheWrite: 0.001, total: 0.034 },
152
+ },
153
+ },
154
+ ]);
155
+
156
+ snapshotUnitMetrics(ctx, "research-slice", "M001/S02", Date.now() - 3000, "claude-sonnet-4-20250514");
157
+
158
+ // Verify both units persisted
159
+ resetMetrics();
160
+ initMetrics(tmpBase);
161
+ const final = getLedger()!;
162
+ assertEq(final.units.length, 2, "2 units after second snapshot");
163
+ }
164
+
165
+ console.log("\n=== File content verification ===");
166
+
167
+ {
168
+ const raw = readFileSync(join(tmpBase, ".gsd", "metrics.json"), "utf-8");
169
+ const parsed: MetricsLedger = JSON.parse(raw);
170
+ assertEq(parsed.version, 1, "file version is 1");
171
+ assertEq(parsed.units.length, 2, "file has 2 units");
172
+ assert(parsed.projectStartedAt > 0, "projectStartedAt is set");
173
+ }
174
+
175
+ console.log("\n=== Empty session handling ===");
176
+
177
+ {
178
+ resetMetrics();
179
+ initMetrics(tmpBase);
180
+
181
+ // Empty session — no messages
182
+ const ctx = mockCtx([]);
183
+ const unit = snapshotUnitMetrics(ctx, "plan-slice", "M001/S01", Date.now(), "test-model");
184
+ assert(unit === null, "returns null for empty session");
185
+
186
+ // Ledger shouldn't have grown
187
+ assertEq(getLedger()!.units.length, 2, "still 2 units (empty session not added)");
188
+ }
189
+
190
+ // ─── Cleanup ──────────────────────────────────────────────────────────────────
191
+
192
+ resetMetrics();
193
+ rmSync(tmpBase, { recursive: true, force: true });
194
+
195
+ console.log(`\n${"=".repeat(40)}`);
196
+ console.log(`Results: ${passed} passed, ${failed} failed`);
197
+ if (failed > 0) {
198
+ process.exit(1);
199
+ } else {
200
+ console.log("All tests passed ✓");
201
+ }