@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,171 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ // loadPrompt reads from ~/.gsd/agent/extensions/gsd/prompts/ (main checkout).
6
+ // In a worktree the file may not exist there yet, so we resolve prompts
7
+ // relative to this test file's location (the worktree copy).
8
+ const __dirname = dirname(fileURLToPath(import.meta.url));
9
+ const worktreePromptsDir = join(__dirname, "..", "prompts");
10
+
11
+ let passed = 0;
12
+ let failed = 0;
13
+
14
+ function assert(condition: boolean, message: string): void {
15
+ if (condition) {
16
+ passed++;
17
+ } else {
18
+ failed++;
19
+ console.error(` FAIL: ${message}`);
20
+ }
21
+ }
22
+
23
+ function assertEq<T>(actual: T, expected: T, message: string): void {
24
+ if (JSON.stringify(actual) === JSON.stringify(expected)) {
25
+ passed++;
26
+ } else {
27
+ failed++;
28
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Load a prompt template from the worktree prompts directory
34
+ * and apply variable substitution (mirrors loadPrompt logic).
35
+ */
36
+ function loadPromptFromWorktree(name: string, vars: Record<string, string> = {}): string {
37
+ const path = join(worktreePromptsDir, `${name}.md`);
38
+ let content = readFileSync(path, "utf-8");
39
+ for (const [key, value] of Object.entries(vars)) {
40
+ content = content.replaceAll(`{{${key}}}`, value);
41
+ }
42
+ return content.trim();
43
+ }
44
+
45
+ // ═══════════════════════════════════════════════════════════════════════════
46
+ // Tests
47
+ // ═══════════════════════════════════════════════════════════════════════════
48
+
49
+ async function main(): Promise<void> {
50
+
51
+ // ─── reassess-roadmap prompt loads and substitutes ─────────────────────
52
+ console.log("\n=== reassess-roadmap prompt loads and substitutes ===");
53
+ {
54
+ const testVars = {
55
+ milestoneId: "M099",
56
+ completedSliceId: "S03",
57
+ assessmentAbsPath: ".gsd/milestones/M099/slices/S03/S03-ASSESSMENT.md",
58
+ roadmapPath: ".gsd/milestones/M099/M099-ROADMAP.md",
59
+ inlinedContext: "--- test inlined context block ---",
60
+ };
61
+
62
+ let result: string;
63
+ let threw = false;
64
+ try {
65
+ result = loadPromptFromWorktree("reassess-roadmap", testVars);
66
+ } catch (err) {
67
+ threw = true;
68
+ result = "";
69
+ console.error(` ERROR: loadPrompt threw: ${err}`);
70
+ }
71
+
72
+ assert(!threw, "loadPrompt does not throw for reassess-roadmap");
73
+ assert(typeof result === "string" && result.length > 0, "loadPrompt returns a non-empty string");
74
+
75
+ // Verify all test variables were substituted into the output
76
+ assert(result.includes("M099"), "prompt contains milestoneId 'M099'");
77
+ assert(result.includes("S03"), "prompt contains completedSliceId 'S03'");
78
+ assert(result.includes(".gsd/milestones/M099/slices/S03/S03-ASSESSMENT.md"), "prompt contains assessmentAbsPath");
79
+ assert(result.includes(".gsd/milestones/M099/M099-ROADMAP.md"), "prompt contains roadmapPath");
80
+ assert(result.includes("--- test inlined context block ---"), "prompt contains inlinedContext");
81
+
82
+ // Verify no un-substituted variables remain
83
+ assert(!result.includes("{{milestoneId}}"), "no un-substituted {{milestoneId}}");
84
+ assert(!result.includes("{{completedSliceId}}"), "no un-substituted {{completedSliceId}}");
85
+ assert(!result.includes("{{assessmentAbsPath}}"), "no un-substituted {{assessmentAbsPath}}");
86
+ assert(!result.includes("{{roadmapPath}}"), "no un-substituted {{roadmapPath}}");
87
+ assert(!result.includes("{{inlinedContext}}"), "no un-substituted {{inlinedContext}}");
88
+ }
89
+
90
+ // ─── reassess-roadmap contains coverage-check instruction ─────────────
91
+ console.log("\n=== reassess-roadmap contains coverage-check instruction ===");
92
+ {
93
+ const prompt = loadPromptFromWorktree("reassess-roadmap", {
94
+ milestoneId: "M001",
95
+ completedSliceId: "S01",
96
+ assessmentAbsPath: ".gsd/milestones/M001/slices/S01/S01-ASSESSMENT.md",
97
+ roadmapPath: ".gsd/milestones/M001/M001-ROADMAP.md",
98
+ inlinedContext: "context",
99
+ });
100
+
101
+ // Normalize to lowercase for case-insensitive matching
102
+ const lower = prompt.toLowerCase();
103
+
104
+ // The prompt must mention "each success criterion" or "every success criterion"
105
+ assert(
106
+ lower.includes("each success criterion") || lower.includes("every success criterion"),
107
+ "prompt contains 'each success criterion' or 'every success criterion'"
108
+ );
109
+
110
+ // The prompt must mention "owning slice" or "remaining slice"
111
+ assert(
112
+ lower.includes("owning slice") || lower.includes("remaining slice"),
113
+ "prompt contains 'owning slice' or 'remaining slice'"
114
+ );
115
+
116
+ // The prompt must mention "no remaining owner" or "no owner" or "no slice"
117
+ assert(
118
+ lower.includes("no remaining owner") || lower.includes("no owner") || lower.includes("no slice"),
119
+ "prompt contains 'no remaining owner', 'no owner', or 'no slice'"
120
+ );
121
+
122
+ // The prompt must mention "blocking issue" or "blocking"
123
+ assert(
124
+ lower.includes("blocking issue") || lower.includes("blocking"),
125
+ "prompt contains 'blocking issue' or 'blocking'"
126
+ );
127
+ }
128
+
129
+ // ─── coverage-check requires at-least-one semantics ───────────────────
130
+ console.log("\n=== coverage-check requires at-least-one semantics ===");
131
+ {
132
+ const prompt = loadPromptFromWorktree("reassess-roadmap", {
133
+ milestoneId: "M001",
134
+ completedSliceId: "S01",
135
+ assessmentAbsPath: ".gsd/milestones/M001/slices/S01/S01-ASSESSMENT.md",
136
+ roadmapPath: ".gsd/milestones/M001/M001-ROADMAP.md",
137
+ inlinedContext: "context",
138
+ });
139
+
140
+ const lower = prompt.toLowerCase();
141
+
142
+ // The instruction must use "at least one" or equivalent inclusive language
143
+ assert(
144
+ lower.includes("at least one") || lower.includes("at-least-one") || lower.includes("one or more"),
145
+ "prompt uses 'at least one' or equivalent inclusive language for slice ownership"
146
+ );
147
+
148
+ // The instruction must NOT require "exactly one" — that would be too rigid
149
+ assert(
150
+ !lower.includes("exactly one owner") && !lower.includes("exactly one slice"),
151
+ "prompt does NOT use 'exactly one' for slice ownership (would be too rigid)"
152
+ );
153
+ }
154
+
155
+ // ═════════════════════════════════════════════════════════════════════════
156
+ // Results
157
+ // ═════════════════════════════════════════════════════════════════════════
158
+
159
+ console.log(`\n${"=".repeat(40)}`);
160
+ console.log(`Results: ${passed} passed, ${failed} failed`);
161
+ if (failed > 0) {
162
+ process.exit(1);
163
+ } else {
164
+ console.log("All tests passed ✓");
165
+ }
166
+ }
167
+
168
+ main().catch((error) => {
169
+ console.error(error);
170
+ process.exit(1);
171
+ });
@@ -0,0 +1,155 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { parseSlackReply, parseDiscordResponse } from "../../remote-questions/format.ts";
4
+ import { resolveRemoteConfig, isValidChannelId } from "../../remote-questions/config.ts";
5
+ import { sanitizeError } from "../../remote-questions/manager.ts";
6
+
7
+ test("parseSlackReply handles single-number single-question answers", () => {
8
+ const result = parseSlackReply("2", [{
9
+ id: "choice",
10
+ header: "Choice",
11
+ question: "Pick one",
12
+ allowMultiple: false,
13
+ options: [
14
+ { label: "Alpha", description: "A" },
15
+ { label: "Beta", description: "B" },
16
+ ],
17
+ }]);
18
+
19
+ assert.deepEqual(result, { answers: { choice: { answers: ["Beta"] } } });
20
+ });
21
+
22
+ test("parseSlackReply handles multiline multi-question answers", () => {
23
+ const result = parseSlackReply("1\ncustom note", [
24
+ {
25
+ id: "first",
26
+ header: "First",
27
+ question: "Pick one",
28
+ allowMultiple: false,
29
+ options: [
30
+ { label: "Alpha", description: "A" },
31
+ { label: "Beta", description: "B" },
32
+ ],
33
+ },
34
+ {
35
+ id: "second",
36
+ header: "Second",
37
+ question: "Explain",
38
+ allowMultiple: false,
39
+ options: [
40
+ { label: "Gamma", description: "G" },
41
+ { label: "Delta", description: "D" },
42
+ ],
43
+ },
44
+ ]);
45
+
46
+ assert.deepEqual(result, {
47
+ answers: {
48
+ first: { answers: ["Alpha"] },
49
+ second: { answers: [], user_note: "custom note" },
50
+ },
51
+ });
52
+ });
53
+
54
+ test("parseDiscordResponse handles single-question reactions", () => {
55
+ const result = parseDiscordResponse([{ emoji: "2️⃣", count: 1 }], null, [{
56
+ id: "choice",
57
+ header: "Choice",
58
+ question: "Pick one",
59
+ allowMultiple: false,
60
+ options: [
61
+ { label: "Alpha", description: "A" },
62
+ { label: "Beta", description: "B" },
63
+ ],
64
+ }]);
65
+
66
+ assert.deepEqual(result, { answers: { choice: { answers: ["Beta"] } } });
67
+ });
68
+
69
+ test("parseDiscordResponse rejects multi-question reaction parsing", () => {
70
+ const result = parseDiscordResponse([{ emoji: "1️⃣", count: 1 }], null, [
71
+ {
72
+ id: "first",
73
+ header: "First",
74
+ question: "Pick one",
75
+ allowMultiple: false,
76
+ options: [{ label: "Alpha", description: "A" }],
77
+ },
78
+ {
79
+ id: "second",
80
+ header: "Second",
81
+ question: "Pick one",
82
+ allowMultiple: false,
83
+ options: [{ label: "Beta", description: "B" }],
84
+ },
85
+ ]);
86
+
87
+ assert.match(String(result.answers.first.user_note), /single-question prompts/i);
88
+ assert.match(String(result.answers.second.user_note), /single-question prompts/i);
89
+ });
90
+
91
+ test("parseSlackReply truncates user_note longer than 500 chars", () => {
92
+ const longText = "x".repeat(600);
93
+ const result = parseSlackReply(longText, [{
94
+ id: "q1",
95
+ header: "Q1",
96
+ question: "Pick",
97
+ allowMultiple: false,
98
+ options: [{ label: "A", description: "a" }],
99
+ }]);
100
+
101
+ const note = result.answers.q1.user_note!;
102
+ assert.ok(note.length <= 502, `note should be truncated, got ${note.length} chars`);
103
+ assert.ok(note.endsWith("…"), "truncated note should end with ellipsis");
104
+ });
105
+
106
+ test("isValidChannelId rejects invalid Slack channel IDs", () => {
107
+ // Too short
108
+ assert.equal(isValidChannelId("slack", "C123"), false);
109
+ // Contains invalid chars (URL injection)
110
+ assert.equal(isValidChannelId("slack", "https://evil.com"), false);
111
+ // Lowercase
112
+ assert.equal(isValidChannelId("slack", "c12345678"), false);
113
+ // Too long
114
+ assert.equal(isValidChannelId("slack", "C1234567890AB"), false);
115
+ // Valid: 9-12 uppercase alphanumeric
116
+ assert.equal(isValidChannelId("slack", "C12345678"), true);
117
+ assert.equal(isValidChannelId("slack", "C12345678AB"), true);
118
+ assert.equal(isValidChannelId("slack", "C1234567890A"), true);
119
+ });
120
+
121
+ test("isValidChannelId rejects invalid Discord channel IDs", () => {
122
+ // Too short
123
+ assert.equal(isValidChannelId("discord", "12345"), false);
124
+ // Contains letters (not a snowflake)
125
+ assert.equal(isValidChannelId("discord", "abc12345678901234"), false);
126
+ // URL injection
127
+ assert.equal(isValidChannelId("discord", "https://evil.com"), false);
128
+ // Too long (21 digits)
129
+ assert.equal(isValidChannelId("discord", "123456789012345678901"), false);
130
+ // Valid: 17-20 digit snowflake
131
+ assert.equal(isValidChannelId("discord", "12345678901234567"), true);
132
+ assert.equal(isValidChannelId("discord", "11234567890123456789"), true);
133
+ });
134
+
135
+ test("sanitizeError strips Slack token patterns from error messages", () => {
136
+ assert.equal(
137
+ sanitizeError("Auth failed: xoxb-1234-5678-abcdef"),
138
+ "Auth failed: [REDACTED]",
139
+ );
140
+ assert.equal(
141
+ sanitizeError("Bad token xoxp-abc-def-ghi in request"),
142
+ "Bad token [REDACTED] in request",
143
+ );
144
+ });
145
+
146
+ test("sanitizeError strips long opaque secrets", () => {
147
+ const fakeDiscordToken = "MTIzNDU2Nzg5MDEyMzQ1Njc4OQ.G1x2y3.abcdefghijklmnop";
148
+ assert.ok(!sanitizeError(`Token: ${fakeDiscordToken}`).includes(fakeDiscordToken));
149
+ });
150
+
151
+ test("sanitizeError preserves short safe messages", () => {
152
+ assert.equal(sanitizeError("HTTP 401: Unauthorized"), "HTTP 401: Unauthorized");
153
+ assert.equal(sanitizeError("Connection refused"), "Connection refused");
154
+ });
155
+
@@ -0,0 +1,99 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdirSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { createPromptRecord, writePromptRecord } from "../../remote-questions/store.ts";
7
+ import { getLatestPromptSummary } from "../../remote-questions/status.ts";
8
+
9
+ function withTempHome(fn: (tempHome: string) => void | Promise<void>) {
10
+ return async () => {
11
+ const savedHome = process.env.HOME;
12
+ const savedUserProfile = process.env.USERPROFILE;
13
+ const tempHome = join(tmpdir(), `gsd-remote-status-${Date.now()}-${Math.random().toString(36).slice(2)}`);
14
+ mkdirSync(join(tempHome, ".gsd", "runtime", "remote-questions"), { recursive: true });
15
+ process.env.HOME = tempHome;
16
+ process.env.USERPROFILE = tempHome;
17
+ try {
18
+ await fn(tempHome);
19
+ } finally {
20
+ process.env.HOME = savedHome;
21
+ process.env.USERPROFILE = savedUserProfile;
22
+ rmSync(tempHome, { recursive: true, force: true });
23
+ }
24
+ };
25
+ }
26
+
27
+ test("getLatestPromptSummary returns latest stored prompt", withTempHome(() => {
28
+ const recordA = createPromptRecord({
29
+ id: "a-prompt",
30
+ channel: "slack",
31
+ createdAt: 1,
32
+ timeoutAt: 10,
33
+ pollIntervalMs: 5000,
34
+ questions: [],
35
+ });
36
+ recordA.updatedAt = 1;
37
+ writePromptRecord(recordA);
38
+
39
+ const recordB = createPromptRecord({
40
+ id: "z-prompt",
41
+ channel: "discord",
42
+ createdAt: 2,
43
+ timeoutAt: 10,
44
+ pollIntervalMs: 5000,
45
+ questions: [],
46
+ });
47
+ recordB.updatedAt = 2;
48
+ recordB.status = "answered";
49
+ writePromptRecord(recordB);
50
+
51
+ const latest = getLatestPromptSummary();
52
+ assert.equal(latest?.id, "z-prompt");
53
+ assert.equal(latest?.status, "answered");
54
+ }));
55
+
56
+ test("getLatestPromptSummary sorts by updatedAt, not filename", withTempHome(() => {
57
+ // Record with alphabetically-LAST id but OLDEST timestamp
58
+ const old = createPromptRecord({
59
+ id: "zzz-oldest",
60
+ channel: "slack",
61
+ createdAt: 1000,
62
+ timeoutAt: 9999,
63
+ pollIntervalMs: 5000,
64
+ questions: [],
65
+ });
66
+ old.updatedAt = 1000;
67
+ writePromptRecord(old);
68
+
69
+ // Record with alphabetically-FIRST id but NEWEST timestamp
70
+ const newest = createPromptRecord({
71
+ id: "aaa-newest",
72
+ channel: "discord",
73
+ createdAt: 3000,
74
+ timeoutAt: 9999,
75
+ pollIntervalMs: 5000,
76
+ questions: [],
77
+ });
78
+ newest.updatedAt = 3000;
79
+ newest.status = "answered";
80
+ writePromptRecord(newest);
81
+
82
+ // Record in between
83
+ const middle = createPromptRecord({
84
+ id: "mmm-middle",
85
+ channel: "slack",
86
+ createdAt: 2000,
87
+ timeoutAt: 9999,
88
+ pollIntervalMs: 5000,
89
+ questions: [],
90
+ });
91
+ middle.updatedAt = 2000;
92
+ writePromptRecord(middle);
93
+
94
+ const latest = getLatestPromptSummary();
95
+ // Should return "aaa-newest" (updatedAt=3000), NOT "zzz-oldest" (alphabetically last)
96
+ assert.equal(latest?.id, "aaa-newest", "should pick the most recently updated prompt, not the alphabetically last filename");
97
+ assert.equal(latest?.status, "answered");
98
+ assert.equal(latest?.updatedAt, 3000);
99
+ }));