@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,521 @@
1
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ import { parseSummary } from '../files.ts';
7
+ import { deriveState } from '../state.ts';
8
+
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const worktreePromptsDir = join(__dirname, '..', 'prompts');
11
+
12
+ /**
13
+ * Load a prompt template from the worktree prompts directory
14
+ * and apply variable substitution (mirrors loadPrompt logic).
15
+ */
16
+ function loadPromptFromWorktree(name: string, vars: Record<string, string> = {}): string {
17
+ const path = join(worktreePromptsDir, `${name}.md`);
18
+ let content = readFileSync(path, 'utf-8');
19
+ for (const [key, value] of Object.entries(vars)) {
20
+ content = content.replaceAll(`{{${key}}}`, value);
21
+ }
22
+ return content.trim();
23
+ }
24
+
25
+ let passed = 0;
26
+ let failed = 0;
27
+
28
+ function assert(condition: boolean, message: string): void {
29
+ if (condition) {
30
+ passed++;
31
+ } else {
32
+ failed++;
33
+ console.error(` FAIL: ${message}`);
34
+ }
35
+ }
36
+
37
+ function assertEq<T>(actual: T, expected: T, message: string): void {
38
+ if (JSON.stringify(actual) === JSON.stringify(expected)) {
39
+ passed++;
40
+ } else {
41
+ failed++;
42
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
43
+ }
44
+ }
45
+
46
+ // ─── Fixture Helpers ───────────────────────────────────────────────────────
47
+
48
+ function createFixtureBase(): string {
49
+ const base = mkdtempSync(join(tmpdir(), 'gsd-replan-test-'));
50
+ mkdirSync(join(base, '.gsd', 'milestones'), { recursive: true });
51
+ return base;
52
+ }
53
+
54
+ function writeRoadmap(base: string, mid: string, content: string): void {
55
+ const dir = join(base, '.gsd', 'milestones', mid);
56
+ mkdirSync(dir, { recursive: true });
57
+ writeFileSync(join(dir, `${mid}-ROADMAP.md`), content);
58
+ }
59
+
60
+ function writePlan(base: string, mid: string, sid: string, content: string): void {
61
+ const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
62
+ mkdirSync(join(dir, 'tasks'), { recursive: true });
63
+ writeFileSync(join(dir, `${sid}-PLAN.md`), content);
64
+ }
65
+
66
+ function writeTaskSummary(base: string, mid: string, sid: string, tid: string, content: string): void {
67
+ const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid, 'tasks');
68
+ mkdirSync(dir, { recursive: true });
69
+ writeFileSync(join(dir, `${tid}-SUMMARY.md`), content);
70
+ }
71
+
72
+ function writeReplanFile(base: string, mid: string, sid: string, content: string): void {
73
+ const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
74
+ mkdirSync(dir, { recursive: true });
75
+ writeFileSync(join(dir, `${sid}-REPLAN.md`), content);
76
+ }
77
+
78
+ /** Standard roadmap with one slice having no dependencies */
79
+ const ROADMAP_ONE_SLICE = `# M001: Test Milestone
80
+
81
+ **Vision:** Test vision.
82
+
83
+ ## Slices
84
+
85
+ - [ ] **S01: Test Slice** \`risk:low\` \`depends:[]\`
86
+ > After this: stuff works
87
+ `;
88
+
89
+ /** Plan with T01 done, T02 not done */
90
+ function makePlanT01DoneT02Pending(): string {
91
+ return `# S01: Test Slice
92
+
93
+ **Goal:** Do things.
94
+ **Demo:** It works.
95
+
96
+ ## Tasks
97
+
98
+ - [x] **T01: First task** \`est:15m\`
99
+ First task description.
100
+
101
+ - [ ] **T02: Second task** \`est:15m\`
102
+ Second task description.
103
+ `;
104
+ }
105
+
106
+ /** Plan with T01 and T02 done, T03 not done */
107
+ function makePlanT01T02DoneT03Pending(): string {
108
+ return `# S01: Test Slice
109
+
110
+ **Goal:** Do things.
111
+ **Demo:** It works.
112
+
113
+ ## Tasks
114
+
115
+ - [x] **T01: First task** \`est:15m\`
116
+ First task description.
117
+
118
+ - [x] **T02: Second task** \`est:15m\`
119
+ Second task description.
120
+
121
+ - [ ] **T03: Third task** \`est:15m\`
122
+ Third task description.
123
+ `;
124
+ }
125
+
126
+ /** Minimal task summary with blocker_discovered flag */
127
+ function makeTaskSummary(tid: string, blockerDiscovered: boolean): string {
128
+ return `---
129
+ id: ${tid}
130
+ parent: S01
131
+ milestone: M001
132
+ provides: []
133
+ key_files: []
134
+ key_decisions: []
135
+ patterns_established: []
136
+ observability_surfaces: []
137
+ duration: 15min
138
+ verification_result: passed
139
+ completed_at: 2025-03-10T12:00:00Z
140
+ blocker_discovered: ${blockerDiscovered}
141
+ ---
142
+
143
+ # ${tid}: Test Task
144
+
145
+ **Did something.**
146
+
147
+ ## What Happened
148
+
149
+ Work was done.
150
+ `;
151
+ }
152
+
153
+ // ═══════════════════════════════════════════════════════════════════════════
154
+ // Parser Extraction: blocker_discovered
155
+ // ═══════════════════════════════════════════════════════════════════════════
156
+
157
+ console.log('\n=== parseSummary: blocker_discovered true (string) ===');
158
+ {
159
+ const content = `---
160
+ id: T01
161
+ parent: S03
162
+ milestone: M002
163
+ blocker_discovered: true
164
+ completed_at: 2025-03-10T12:00:00Z
165
+ ---
166
+
167
+ # T01: Test Task
168
+
169
+ **One-liner.**
170
+
171
+ ## What Happened
172
+
173
+ Found a blocker.
174
+ `;
175
+
176
+ const s = parseSummary(content);
177
+ assertEq(s.frontmatter.blocker_discovered, true, 'blocker_discovered: true (string) extracts as true');
178
+ }
179
+
180
+ console.log('\n=== parseSummary: blocker_discovered false (string) ===');
181
+ {
182
+ const content = `---
183
+ id: T02
184
+ parent: S03
185
+ milestone: M002
186
+ blocker_discovered: false
187
+ completed_at: 2025-03-10T12:00:00Z
188
+ ---
189
+
190
+ # T02: Normal Task
191
+
192
+ **One-liner.**
193
+
194
+ ## What Happened
195
+
196
+ No blocker.
197
+ `;
198
+
199
+ const s = parseSummary(content);
200
+ assertEq(s.frontmatter.blocker_discovered, false, 'blocker_discovered: false extracts as false');
201
+ }
202
+
203
+ console.log('\n=== parseSummary: blocker_discovered missing (defaults to false) ===');
204
+ {
205
+ const content = `---
206
+ id: T03
207
+ parent: S03
208
+ milestone: M002
209
+ completed_at: 2025-03-10T12:00:00Z
210
+ ---
211
+
212
+ # T03: No Blocker Field
213
+
214
+ **One-liner.**
215
+
216
+ ## What Happened
217
+
218
+ No blocker field at all.
219
+ `;
220
+
221
+ const s = parseSummary(content);
222
+ assertEq(s.frontmatter.blocker_discovered, false, 'blocker_discovered missing defaults to false');
223
+ }
224
+
225
+ console.log('\n=== parseSummary: blocker_discovered true (boolean from YAML) ===');
226
+ {
227
+ // YAML parsers may deliver `true` as a boolean rather than the string "true"
228
+ // We test this via a summary that has blocker_discovered: true with no quotes
229
+ // The YAML parser in parseFrontmatterMap may return boolean true directly
230
+ const content = `---
231
+ id: T04
232
+ parent: S03
233
+ milestone: M002
234
+ blocker_discovered: true
235
+ completed_at: 2025-03-10T12:00:00Z
236
+ ---
237
+
238
+ # T04: Boolean True
239
+
240
+ **One-liner.**
241
+
242
+ ## What Happened
243
+
244
+ Blocker as boolean.
245
+ `;
246
+
247
+ const s = parseSummary(content);
248
+ assertEq(s.frontmatter.blocker_discovered, true, 'blocker_discovered: true (YAML boolean) extracts as true');
249
+ }
250
+
251
+ console.log('\n=== parseSummary: blocker_discovered with full frontmatter ===');
252
+ {
253
+ const content = `---
254
+ id: T05
255
+ parent: S03
256
+ milestone: M002
257
+ provides:
258
+ - something
259
+ requires: []
260
+ affects: []
261
+ key_files:
262
+ - files.ts
263
+ key_decisions: []
264
+ patterns_established: []
265
+ drill_down_paths: []
266
+ observability_surfaces: []
267
+ duration: 15min
268
+ verification_result: passed
269
+ completed_at: 2025-03-10T12:00:00Z
270
+ blocker_discovered: true
271
+ ---
272
+
273
+ # T05: Full Frontmatter With Blocker
274
+
275
+ **Found an architectural mismatch.**
276
+
277
+ ## What Happened
278
+
279
+ The API doesn't support what we assumed.
280
+
281
+ ## Deviations
282
+
283
+ Major deviation from plan.
284
+
285
+ ## Files Created/Modified
286
+
287
+ - \`files.ts\` — attempted changes
288
+ `;
289
+
290
+ const s = parseSummary(content);
291
+ assertEq(s.frontmatter.blocker_discovered, true, 'blocker_discovered true with full frontmatter');
292
+ assertEq(s.frontmatter.id, 'T05', 'other fields still parse correctly alongside blocker_discovered');
293
+ assertEq(s.frontmatter.duration, '15min', 'duration still parsed');
294
+ assertEq(s.frontmatter.provides[0], 'something', 'provides still parsed');
295
+ }
296
+
297
+ // ═══════════════════════════════════════════════════════════════════════════
298
+ // State Detection: replanning-slice phase
299
+ // ═══════════════════════════════════════════════════════════════════════════
300
+
301
+ // (a) blocker found + no REPLAN.md → replanning-slice
302
+ console.log('\n=== deriveState: blocker found, no REPLAN → replanning-slice ===');
303
+ {
304
+ const base = createFixtureBase();
305
+ writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
306
+ writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
307
+ writeTaskSummary(base, 'M001', 'S01', 'T01', makeTaskSummary('T01', true));
308
+
309
+ const state = await deriveState(base);
310
+ assertEq(state.phase, 'replanning-slice', 'phase is replanning-slice when blocker found and no REPLAN.md');
311
+ assert(state.nextAction.includes('T01'), 'nextAction mentions blocker task T01');
312
+ assert(state.nextAction.includes('blocker_discovered'), 'nextAction mentions blocker_discovered');
313
+ assertEq(state.activeTask?.id, 'T02', 'activeTask is still T02 (the next incomplete task)');
314
+ assert(state.blockers.length > 0, 'blockers array is non-empty');
315
+ rmSync(base, { recursive: true, force: true });
316
+ }
317
+
318
+ // (b) blocker found + REPLAN.md exists → executing (loop protection)
319
+ console.log('\n=== deriveState: blocker found + REPLAN exists → executing (loop protection) ===');
320
+ {
321
+ const base = createFixtureBase();
322
+ writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
323
+ writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
324
+ writeTaskSummary(base, 'M001', 'S01', 'T01', makeTaskSummary('T01', true));
325
+ writeReplanFile(base, 'M001', 'S01', '# Replan\n\nAlready replanned.');
326
+
327
+ const state = await deriveState(base);
328
+ assertEq(state.phase, 'executing', 'phase is executing when REPLAN.md exists (loop protection)');
329
+ assertEq(state.activeTask?.id, 'T02', 'activeTask is T02');
330
+ rmSync(base, { recursive: true, force: true });
331
+ }
332
+
333
+ // (c) no blocker → executing
334
+ console.log('\n=== deriveState: no blocker in completed tasks → executing ===');
335
+ {
336
+ const base = createFixtureBase();
337
+ writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
338
+ writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
339
+ writeTaskSummary(base, 'M001', 'S01', 'T01', makeTaskSummary('T01', false));
340
+
341
+ const state = await deriveState(base);
342
+ assertEq(state.phase, 'executing', 'phase is executing when no blocker found');
343
+ assertEq(state.activeTask?.id, 'T02', 'activeTask is T02');
344
+ rmSync(base, { recursive: true, force: true });
345
+ }
346
+
347
+ // (d) multiple completed tasks, one with blocker → replanning-slice
348
+ console.log('\n=== deriveState: multiple completed tasks, one blocker → replanning-slice ===');
349
+ {
350
+ const base = createFixtureBase();
351
+ writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
352
+ writePlan(base, 'M001', 'S01', makePlanT01T02DoneT03Pending());
353
+ writeTaskSummary(base, 'M001', 'S01', 'T01', makeTaskSummary('T01', false));
354
+ writeTaskSummary(base, 'M001', 'S01', 'T02', makeTaskSummary('T02', true));
355
+
356
+ const state = await deriveState(base);
357
+ assertEq(state.phase, 'replanning-slice', 'phase is replanning-slice when T02 has blocker');
358
+ assert(state.nextAction.includes('T02'), 'nextAction mentions blocker task T02');
359
+ assertEq(state.activeTask?.id, 'T03', 'activeTask is T03 (next incomplete)');
360
+ rmSync(base, { recursive: true, force: true });
361
+ }
362
+
363
+ // (e) completed task with no summary file → executing (gracefully skipped)
364
+ console.log('\n=== deriveState: completed task with no summary file → executing ===');
365
+ {
366
+ const base = createFixtureBase();
367
+ writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
368
+ writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
369
+ // No summary file written for T01
370
+
371
+ const state = await deriveState(base);
372
+ assertEq(state.phase, 'executing', 'phase is executing when completed task has no summary');
373
+ rmSync(base, { recursive: true, force: true });
374
+ }
375
+
376
+ // ═══════════════════════════════════════════════════════════════════════════
377
+ // Prompt: replan-slice template loading and substitution
378
+ // ═══════════════════════════════════════════════════════════════════════════
379
+
380
+ console.log('\n=== prompt: replan-slice template loads and substitutes variables ===');
381
+ {
382
+ const prompt = loadPromptFromWorktree('replan-slice', {
383
+ milestoneId: 'M001',
384
+ sliceId: 'S01',
385
+ sliceTitle: 'Test Slice',
386
+ slicePath: '.gsd/milestones/M001/slices/S01',
387
+ planPath: '.gsd/milestones/M001/slices/S01/S01-PLAN.md',
388
+ inlinedContext: '## Inlined Context\n\nTest context here.',
389
+ });
390
+
391
+ assert(prompt.includes('M001'), 'prompt contains milestoneId');
392
+ assert(prompt.includes('S01'), 'prompt contains sliceId');
393
+ assert(prompt.includes('Test Slice'), 'prompt contains sliceTitle');
394
+ assert(prompt.includes('.gsd/milestones/M001/slices/S01/S01-PLAN.md'), 'prompt contains planPath');
395
+ assert(prompt.includes('Test context here'), 'prompt contains inlined context');
396
+ }
397
+
398
+ console.log('\n=== prompt: replan-slice contains preserve-completed-tasks instruction ===');
399
+ {
400
+ const prompt = loadPromptFromWorktree('replan-slice', {
401
+ milestoneId: 'M001',
402
+ sliceId: 'S01',
403
+ sliceTitle: 'Test Slice',
404
+ slicePath: '.gsd/milestones/M001/slices/S01',
405
+ planPath: '.gsd/milestones/M001/slices/S01/S01-PLAN.md',
406
+ blockerTaskId: 'T01',
407
+ inlinedContext: '',
408
+ });
409
+
410
+ assert(prompt.includes('Do NOT renumber or remove completed tasks'), 'prompt contains preserve-completed-tasks instruction');
411
+ assert(prompt.includes('[x]'), 'prompt mentions [x] checkmarks');
412
+ assert(prompt.includes('replanAbsPath') || prompt.includes('REPLAN'), 'prompt references replan output path');
413
+ assert(prompt.includes('blocker_discovered'), 'prompt mentions blocker_discovered');
414
+ }
415
+
416
+ // ═══════════════════════════════════════════════════════════════════════════
417
+ // Dispatch: diagnoseExpectedArtifact for replan-slice
418
+ // ═══════════════════════════════════════════════════════════════════════════
419
+
420
+ console.log('\n=== dispatch: diagnoseExpectedArtifact returns REPLAN.md path ===');
421
+ {
422
+ // We can't import diagnoseExpectedArtifact directly (it's not exported),
423
+ // but we can verify the prompt template has the right structure and
424
+ // the state machine routes correctly. The diagnose function is integration-tested
425
+ // via the dispatch chain. We verify indirectly via state phase detection.
426
+
427
+ // Verify state correctly routes to replanning-slice phase
428
+ const base = createFixtureBase();
429
+ writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
430
+ writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
431
+ writeTaskSummary(base, 'M001', 'S01', 'T01', makeTaskSummary('T01', true));
432
+
433
+ const state = await deriveState(base);
434
+ assertEq(state.phase, 'replanning-slice', 'dispatch: state routes to replanning-slice when blocker found');
435
+ assert(state.activeSlice?.id === 'S01', 'dispatch: activeSlice is S01');
436
+ rmSync(base, { recursive: true, force: true });
437
+ }
438
+
439
+ // ═══════════════════════════════════════════════════════════════════════════
440
+ // Display Functions: unitVerb, unitPhaseLabel, peekNext entries
441
+ // ═══════════════════════════════════════════════════════════════════════════
442
+
443
+ console.log('\n=== display: replan-slice prompt template has correct unit header ===');
444
+ {
445
+ const prompt = loadPromptFromWorktree('replan-slice', {
446
+ milestoneId: 'M001',
447
+ sliceId: 'S01',
448
+ sliceTitle: 'Test Slice',
449
+ slicePath: '.gsd/milestones/M001/slices/S01',
450
+ planPath: '.gsd/milestones/M001/slices/S01/S01-PLAN.md',
451
+ blockerTaskId: 'T01',
452
+ inlinedContext: '',
453
+ });
454
+
455
+ assert(prompt.includes('UNIT: Replan Slice'), 'prompt has Replan Slice unit header');
456
+ assert(prompt.includes('Slice S01 replanned'), 'prompt has completion message');
457
+ }
458
+
459
+ // ═══════════════════════════════════════════════════════════════════════════
460
+ // Doctor: blocker_discovered_no_replan diagnostics
461
+ // ═══════════════════════════════════════════════════════════════════════════
462
+
463
+ import { runGSDDoctor } from '../doctor.ts';
464
+
465
+ // (a) blocker + no REPLAN.md → issue emitted
466
+ console.log('\n=== doctor: blocker + no REPLAN.md → blocker_discovered_no_replan issue ===');
467
+ {
468
+ const base = createFixtureBase();
469
+ writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
470
+ writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
471
+ writeTaskSummary(base, 'M001', 'S01', 'T01', makeTaskSummary('T01', true));
472
+
473
+ const report = await runGSDDoctor(base, { fix: false, scope: 'M001/S01' });
474
+ const blockerIssues = report.issues.filter(i => i.code === 'blocker_discovered_no_replan');
475
+ assert(blockerIssues.length > 0, 'doctor emits blocker_discovered_no_replan when blocker + no REPLAN');
476
+ assert(blockerIssues[0]?.message.includes('T01'), 'issue message mentions the blocker task T01');
477
+ assertEq(blockerIssues[0]?.severity, 'warning', 'blocker_discovered_no_replan is warning severity');
478
+ assertEq(blockerIssues[0]?.scope, 'slice', 'blocker_discovered_no_replan has slice scope');
479
+ rmSync(base, { recursive: true, force: true });
480
+ }
481
+
482
+ // (b) blocker + REPLAN.md exists → no issue
483
+ console.log('\n=== doctor: blocker + REPLAN.md exists → no blocker_discovered_no_replan issue ===');
484
+ {
485
+ const base = createFixtureBase();
486
+ writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
487
+ writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
488
+ writeTaskSummary(base, 'M001', 'S01', 'T01', makeTaskSummary('T01', true));
489
+ writeReplanFile(base, 'M001', 'S01', '# Replan\n\nAlready replanned.');
490
+
491
+ const report = await runGSDDoctor(base, { fix: false, scope: 'M001/S01' });
492
+ const blockerIssues = report.issues.filter(i => i.code === 'blocker_discovered_no_replan');
493
+ assertEq(blockerIssues.length, 0, 'no blocker_discovered_no_replan when REPLAN.md exists');
494
+ rmSync(base, { recursive: true, force: true });
495
+ }
496
+
497
+ // (c) no blocker → no issue
498
+ console.log('\n=== doctor: no blocker → no blocker_discovered_no_replan issue ===');
499
+ {
500
+ const base = createFixtureBase();
501
+ writeRoadmap(base, 'M001', ROADMAP_ONE_SLICE);
502
+ writePlan(base, 'M001', 'S01', makePlanT01DoneT02Pending());
503
+ writeTaskSummary(base, 'M001', 'S01', 'T01', makeTaskSummary('T01', false));
504
+
505
+ const report = await runGSDDoctor(base, { fix: false, scope: 'M001/S01' });
506
+ const blockerIssues = report.issues.filter(i => i.code === 'blocker_discovered_no_replan');
507
+ assertEq(blockerIssues.length, 0, 'no blocker_discovered_no_replan when no blocker');
508
+ rmSync(base, { recursive: true, force: true });
509
+ }
510
+
511
+ // ═══════════════════════════════════════════════════════════════════════════
512
+ // Results
513
+ // ═══════════════════════════════════════════════════════════════════════════
514
+
515
+ console.log(`\n${'='.repeat(40)}`);
516
+ console.log(`Results: ${passed} passed, ${failed} failed`);
517
+ if (failed > 0) {
518
+ process.exit(1);
519
+ } else {
520
+ console.log('All tests passed ✓');
521
+ }
@@ -0,0 +1,125 @@
1
+ import { parseRequirementCounts } from "../files.ts";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { deriveState } from "../state.ts";
6
+ import { runGSDDoctor } from "../doctor.ts";
7
+
8
+ let passed = 0;
9
+ let failed = 0;
10
+
11
+ function assert(condition: boolean, message: string): void {
12
+ if (condition) passed++;
13
+ else {
14
+ failed++;
15
+ console.error(` FAIL: ${message}`);
16
+ }
17
+ }
18
+
19
+ function assertEq<T>(actual: T, expected: T, message: string): void {
20
+ if (JSON.stringify(actual) === JSON.stringify(expected)) passed++;
21
+ else {
22
+ failed++;
23
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
24
+ }
25
+ }
26
+
27
+ console.log("\n=== requirement counts parser ===");
28
+ {
29
+ const counts = parseRequirementCounts(`# Requirements
30
+
31
+ ## Active
32
+
33
+ ### R001 — Foo
34
+ - Status: active
35
+
36
+ ### R002 — Bar
37
+ - Status: blocked
38
+
39
+ ## Validated
40
+
41
+ ### R010 — Baz
42
+ - Status: validated
43
+
44
+ ## Deferred
45
+
46
+ ### R020 — Qux
47
+ - Status: deferred
48
+
49
+ ## Out of Scope
50
+
51
+ ### R030 — No
52
+ - Status: out-of-scope
53
+ `);
54
+ assertEq(counts.active, 2, "counts active requirements by section");
55
+ assertEq(counts.validated, 1, "counts validated requirements");
56
+ assertEq(counts.deferred, 1, "counts deferred requirements");
57
+ assertEq(counts.outOfScope, 1, "counts out of scope requirements");
58
+ assertEq(counts.blocked, 1, "counts blocked statuses");
59
+ }
60
+
61
+ const base = mkdtempSync(join(tmpdir(), "gsd-requirements-test-"));
62
+ const gsd = join(base, ".gsd");
63
+ const mDir = join(gsd, "milestones", "M001");
64
+ const sDir = join(mDir, "slices", "S01");
65
+ const tDir = join(sDir, "tasks");
66
+ mkdirSync(tDir, { recursive: true });
67
+ writeFileSync(join(gsd, "REQUIREMENTS.md"), `# Requirements
68
+
69
+ ## Active
70
+
71
+ ### R001 — Missing owner
72
+ - Class: core-capability
73
+ - Status: active
74
+ - Description: thing
75
+ - Why it matters: thing
76
+ - Source: user
77
+ - Primary owning slice: none yet
78
+ - Supporting slices: none
79
+ - Validation: unmapped
80
+ - Notes: none
81
+
82
+ ## Validated
83
+
84
+ ## Deferred
85
+
86
+ ## Out of Scope
87
+
88
+ ## Traceability
89
+ `, "utf-8");
90
+ writeFileSync(join(mDir, "M001-ROADMAP.md"), `# M001: Demo
91
+
92
+ ## Slices
93
+ - [ ] **S01: Demo Slice** \`risk:low\` \`depends:[]\`
94
+ > After this: demo works
95
+ `, "utf-8");
96
+ writeFileSync(join(sDir, "S01-PLAN.md"), `# S01: Demo Slice
97
+
98
+ **Goal:** Demo
99
+ **Demo:** Demo
100
+
101
+ ## Must-Haves
102
+ - done
103
+
104
+ ## Tasks
105
+ - [ ] **T01: Implement thing** \`est:10m\`
106
+ Task is in progress.
107
+ `, "utf-8");
108
+
109
+ console.log("\n=== deriveState includes requirements counts ===");
110
+ {
111
+ const state = await deriveState(base);
112
+ assert(state.requirements !== undefined, "state includes requirements summary");
113
+ assertEq(state.requirements?.active, 1, "state reports active requirement count");
114
+ }
115
+
116
+ console.log("\n=== doctor flags orphaned active requirement ===");
117
+ {
118
+ const report = await runGSDDoctor(base);
119
+ assert(report.issues.some(issue => issue.code === "active_requirement_missing_owner"), "doctor flags missing owner");
120
+ }
121
+
122
+ rmSync(base, { recursive: true, force: true });
123
+ console.log(`\nResults: ${passed} passed, ${failed} failed`);
124
+ if (failed > 0) process.exit(1);
125
+ console.log("All tests passed ✓");
@@ -0,0 +1,34 @@
1
+ // ESM resolve hook: .js → .ts rewriting for test environments.
2
+ // Only rewrites relative imports from our own source files — not from node_modules.
3
+ //
4
+ // Handles two patterns:
5
+ // 1. .js → .ts (pi bundler convention: source files use .js specifiers)
6
+ // 2. extensionless → .ts (some source files omit extensions in relative imports)
7
+
8
+ export function resolve(specifier, context, nextResolve) {
9
+ const parentURL = context.parentURL || '';
10
+ const isFromNodeModules = parentURL.includes('/node_modules/');
11
+
12
+ if (!isFromNodeModules && !specifier.startsWith('node:')) {
13
+ // Rewrite .js → .ts
14
+ if (specifier.endsWith('.js')) {
15
+ const tsSpecifier = specifier.replace(/\.js$/, '.ts');
16
+ try {
17
+ return nextResolve(tsSpecifier, context);
18
+ } catch {
19
+ // fall through to default resolution
20
+ }
21
+ }
22
+
23
+ // Try adding .ts to extensionless relative imports
24
+ if (specifier.startsWith('.') && !/\.[a-z]+$/i.test(specifier)) {
25
+ try {
26
+ return nextResolve(specifier + '.ts', context);
27
+ } catch {
28
+ // fall through to default resolution
29
+ }
30
+ }
31
+ }
32
+
33
+ return nextResolve(specifier, context);
34
+ }
@@ -0,0 +1,11 @@
1
+ // Custom ESM resolver: rewrites .js imports to .ts for node --test with TypeScript sources.
2
+ // Usage: node --import ./agent/extensions/gsd/tests/resolve-ts.mjs --test ...
3
+ //
4
+ // This is needed because pi extension source files use .js import specifiers
5
+ // (the pi runtime bundler convention), but only .ts files exist on disk.
6
+ // Node's built-in TypeScript support strips types but doesn't rewrite specifiers.
7
+
8
+ import { register } from 'node:module';
9
+ import { pathToFileURL } from 'node:url';
10
+
11
+ register(new URL('./resolve-ts-hooks.mjs', import.meta.url), pathToFileURL('./'));