@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,348 @@
1
+ // Tests for extractUatType — the core UAT classification primitive — plus
2
+ // prompt template loading and dispatch precondition assertions (via
3
+ // resolveSliceFile / extractUatType on real fixture files).
4
+ //
5
+ // Sections:
6
+ // (a)–(j) extractUatType classification (17 assertions from T01)
7
+ // (k) run-uat prompt template loading and content integrity (8 assertions)
8
+ // (l) dispatch precondition assertions via resolveSliceFile (4 assertions)
9
+
10
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
11
+ import { join, dirname } from 'node:path';
12
+ import { tmpdir } from 'node:os';
13
+ import { fileURLToPath } from 'node:url';
14
+
15
+ import { extractUatType } from '../files.ts';
16
+ import { resolveSliceFile } from '../paths.ts';
17
+
18
+ // ─── Worktree-aware prompt loader ──────────────────────────────────────────
19
+ // Resolves prompts relative to this test file so the worktree copy is used
20
+ // instead of the main checkout copy (matches complete-milestone.test.ts pattern).
21
+
22
+ const __dirname = dirname(fileURLToPath(import.meta.url));
23
+ const worktreePromptsDir = join(__dirname, '..', 'prompts');
24
+
25
+ function loadPromptFromWorktree(name: string, vars: Record<string, string> = {}): string {
26
+ const path = join(worktreePromptsDir, `${name}.md`);
27
+ let content = readFileSync(path, 'utf-8');
28
+ for (const [key, value] of Object.entries(vars)) {
29
+ content = content.replaceAll(`{{${key}}}`, value);
30
+ }
31
+ return content.trim();
32
+ }
33
+
34
+ // ─── Assertion helpers ─────────────────────────────────────────────────────
35
+
36
+ let passed = 0;
37
+ let failed = 0;
38
+
39
+ function assert(condition: boolean, message: string): void {
40
+ if (condition) {
41
+ passed++;
42
+ } else {
43
+ failed++;
44
+ console.error(` FAIL: ${message}`);
45
+ }
46
+ }
47
+
48
+ function assertEq<T>(actual: T, expected: T, message: string): void {
49
+ if (JSON.stringify(actual) === JSON.stringify(expected)) {
50
+ passed++;
51
+ } else {
52
+ failed++;
53
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
54
+ }
55
+ }
56
+
57
+ // ─── Fixture helpers ───────────────────────────────────────────────────────
58
+
59
+ function createFixtureBase(): string {
60
+ const base = mkdtempSync(join(tmpdir(), 'gsd-run-uat-test-'));
61
+ mkdirSync(join(base, '.gsd', 'milestones'), { recursive: true });
62
+ return base;
63
+ }
64
+
65
+ function writeSliceFile(
66
+ base: string,
67
+ mid: string,
68
+ sid: string,
69
+ suffix: string,
70
+ content: string,
71
+ ): void {
72
+ const dir = join(base, '.gsd', 'milestones', mid, 'slices', sid);
73
+ mkdirSync(dir, { recursive: true });
74
+ writeFileSync(join(dir, `${sid}-${suffix}.md`), content);
75
+ }
76
+
77
+ function cleanup(base: string): void {
78
+ rmSync(base, { recursive: true, force: true });
79
+ }
80
+
81
+ function makeUatContent(mode: string): string {
82
+ return `# UAT File\n\n## UAT Type\n\n- UAT mode: ${mode}\n- Some other bullet: value\n`;
83
+ }
84
+
85
+ // ═══════════════════════════════════════════════════════════════════════════
86
+ // Tests
87
+ // ═══════════════════════════════════════════════════════════════════════════
88
+
89
+ async function main(): Promise<void> {
90
+
91
+ // ─── (a) artifact-driven ──────────────────────────────────────────────────
92
+ console.log('\n── (a) artifact-driven');
93
+
94
+ assertEq(
95
+ extractUatType(makeUatContent('artifact-driven')),
96
+ 'artifact-driven',
97
+ 'plain artifact-driven → artifact-driven',
98
+ );
99
+
100
+ assertEq(
101
+ extractUatType('## UAT Type\n\n- UAT mode: artifact-driven\n'),
102
+ 'artifact-driven',
103
+ 'minimal content, artifact-driven',
104
+ );
105
+
106
+ // ─── (b) live-runtime ─────────────────────────────────────────────────────
107
+ console.log('\n── (b) live-runtime');
108
+
109
+ assertEq(
110
+ extractUatType(makeUatContent('live-runtime')),
111
+ 'live-runtime',
112
+ 'plain live-runtime → live-runtime',
113
+ );
114
+
115
+ // ─── (c) human-experience ─────────────────────────────────────────────────
116
+ console.log('\n── (c) human-experience');
117
+
118
+ assertEq(
119
+ extractUatType(makeUatContent('human-experience')),
120
+ 'human-experience',
121
+ 'plain human-experience → human-experience',
122
+ );
123
+
124
+ // ─── (d) mixed standalone ─────────────────────────────────────────────────
125
+ console.log('\n── (d) mixed standalone');
126
+
127
+ assertEq(
128
+ extractUatType(makeUatContent('mixed')),
129
+ 'mixed',
130
+ 'plain mixed → mixed',
131
+ );
132
+
133
+ // ─── (e) mixed with parenthetical ─────────────────────────────────────────
134
+ console.log('\n── (e) mixed parenthetical');
135
+
136
+ assertEq(
137
+ extractUatType(makeUatContent('mixed (artifact-driven + live-runtime)')),
138
+ 'mixed',
139
+ 'mixed (artifact-driven + live-runtime) → mixed (leading keyword only)',
140
+ );
141
+
142
+ assertEq(
143
+ extractUatType(makeUatContent('mixed (some other description)')),
144
+ 'mixed',
145
+ 'mixed with arbitrary parenthetical → mixed',
146
+ );
147
+
148
+ // ─── (f) missing ## UAT Type section ──────────────────────────────────────
149
+ console.log('\n── (f) missing UAT Type section');
150
+
151
+ assertEq(
152
+ extractUatType('# UAT File\n\n## Overview\n\nSome content.\n'),
153
+ undefined,
154
+ 'no ## UAT Type section → undefined',
155
+ );
156
+
157
+ assertEq(
158
+ extractUatType(''),
159
+ undefined,
160
+ 'empty content → undefined',
161
+ );
162
+
163
+ // ─── (g) ## UAT Type present but no UAT mode: bullet ─────────────────────
164
+ console.log('\n── (g) UAT Type section present, no UAT mode: bullet');
165
+
166
+ assertEq(
167
+ extractUatType('## UAT Type\n\n- Some other bullet: value\n- Another bullet\n'),
168
+ undefined,
169
+ 'section present but no UAT mode: bullet → undefined',
170
+ );
171
+
172
+ assertEq(
173
+ extractUatType('## UAT Type\n\n'),
174
+ undefined,
175
+ 'section present but empty → undefined',
176
+ );
177
+
178
+ // ─── (h) unknown keyword ──────────────────────────────────────────────────
179
+ console.log('\n── (h) unknown keyword');
180
+
181
+ assertEq(
182
+ extractUatType(makeUatContent('automated')),
183
+ undefined,
184
+ 'unknown keyword automated → undefined',
185
+ );
186
+
187
+ assertEq(
188
+ extractUatType(makeUatContent('fully-automated')),
189
+ undefined,
190
+ 'unknown keyword fully-automated → undefined',
191
+ );
192
+
193
+ // ─── (i) extra whitespace around value ────────────────────────────────────
194
+ console.log('\n── (i) extra whitespace');
195
+
196
+ assertEq(
197
+ extractUatType('## UAT Type\n\n- UAT mode: artifact-driven \n'),
198
+ 'artifact-driven',
199
+ 'leading/trailing whitespace around value → still classified correctly',
200
+ );
201
+
202
+ assertEq(
203
+ extractUatType('## UAT Type\n\n- UAT mode: mixed (artifact-driven + live-runtime) \n'),
204
+ 'mixed',
205
+ 'whitespace around mixed parenthetical → mixed',
206
+ );
207
+
208
+ // ─── (j) case sensitivity ─────────────────────────────────────────────────
209
+ console.log('\n── (j) case sensitivity');
210
+
211
+ assertEq(
212
+ extractUatType(makeUatContent('Artifact-Driven')),
213
+ 'artifact-driven',
214
+ 'Artifact-Driven (title case) → artifact-driven (function lowercases before matching)',
215
+ );
216
+
217
+ assertEq(
218
+ extractUatType(makeUatContent('MIXED')),
219
+ 'mixed',
220
+ 'MIXED (upper case) → mixed (function lowercases before matching)',
221
+ );
222
+
223
+ // ─── (k) prompt template loading and content integrity ────────────────────
224
+ console.log('\n── (k) run-uat prompt template');
225
+
226
+ const milestoneId = 'M001';
227
+ const sliceId = 'S01';
228
+ const uatPath = '.gsd/milestones/M001/slices/S01/S01-UAT.md';
229
+ const uatResultAbsPath = '/tmp/gsd-test/S01-UAT-RESULT.md';
230
+ const uatResultPath = '.gsd/milestones/M001/slices/S01/S01-UAT-RESULT.md';
231
+ const uatType = 'artifact-driven';
232
+ const inlinedContext = '<!-- no context -->';
233
+
234
+ let promptResult: string | undefined;
235
+ let promptThrew = false;
236
+ try {
237
+ promptResult = loadPromptFromWorktree('run-uat', {
238
+ milestoneId,
239
+ sliceId,
240
+ uatPath,
241
+ uatResultAbsPath,
242
+ uatResultPath,
243
+ uatType,
244
+ inlinedContext,
245
+ });
246
+ } catch {
247
+ promptThrew = true;
248
+ }
249
+
250
+ assert(!promptThrew, 'loadPromptFromWorktree("run-uat", vars) does not throw');
251
+ assert(
252
+ typeof promptResult === 'string' && promptResult.length > 0,
253
+ 'run-uat prompt result is a non-empty string',
254
+ );
255
+ assert(
256
+ promptResult?.includes(milestoneId) ?? false,
257
+ `prompt contains milestoneId value "${milestoneId}" after substitution`,
258
+ );
259
+ assert(
260
+ promptResult?.includes(sliceId) ?? false,
261
+ `prompt contains sliceId value "${sliceId}" after substitution`,
262
+ );
263
+ assert(
264
+ promptResult?.includes(uatResultAbsPath) ?? false,
265
+ `prompt contains uatResultAbsPath value after substitution`,
266
+ );
267
+ assert(
268
+ !/\{\{[^}]+\}\}/.test(promptResult ?? ''),
269
+ 'no unreplaced {{...}} tokens remain after variable substitution',
270
+ );
271
+ assert(
272
+ /artifact|execute|run/i.test(promptResult ?? ''),
273
+ 'prompt contains artifact-driven execution language (artifact/execute/run)',
274
+ );
275
+ assert(
276
+ /surfaced for human review/i.test(promptResult ?? ''),
277
+ 'prompt contains "surfaced for human review" text for non-artifact-driven path',
278
+ );
279
+
280
+ // ─── (l) dispatch precondition assertions via resolveSliceFile ────────────
281
+ console.log('\n── (l) dispatch preconditions via resolveSliceFile');
282
+
283
+ // State A: UAT file exists, UAT-RESULT file does NOT — triggers dispatch
284
+ {
285
+ const base = createFixtureBase();
286
+ const uatContent = makeUatContent('artifact-driven');
287
+ try {
288
+ writeSliceFile(base, 'M001', 'S01', 'UAT', uatContent);
289
+
290
+ const uatFilePath = resolveSliceFile(base, 'M001', 'S01', 'UAT');
291
+ assert(
292
+ uatFilePath !== null,
293
+ 'resolveSliceFile(..., "UAT") returns non-null when UAT file exists (dispatch trigger state)',
294
+ );
295
+
296
+ const uatResultFilePath = resolveSliceFile(base, 'M001', 'S01', 'UAT-RESULT');
297
+ assertEq(
298
+ uatResultFilePath,
299
+ null,
300
+ 'resolveSliceFile(..., "UAT-RESULT") returns null when result file missing (dispatch trigger state)',
301
+ );
302
+
303
+ // End-to-end: file content → parse → classify
304
+ const rawContent = readFileSync(uatFilePath!, 'utf-8');
305
+ assertEq(
306
+ extractUatType(rawContent),
307
+ 'artifact-driven',
308
+ 'extractUatType on fixture UAT file returns expected type (end-to-end data flow)',
309
+ );
310
+ } finally {
311
+ cleanup(base);
312
+ }
313
+ }
314
+
315
+ // State B: UAT-RESULT file exists — dispatch is skipped (idempotent)
316
+ {
317
+ const base = createFixtureBase();
318
+ try {
319
+ writeSliceFile(base, 'M001', 'S01', 'UAT', makeUatContent('artifact-driven'));
320
+ writeSliceFile(base, 'M001', 'S01', 'UAT-RESULT', '# UAT Result\n\nverdict: PASS\n');
321
+
322
+ const uatResultFilePath = resolveSliceFile(base, 'M001', 'S01', 'UAT-RESULT');
323
+ assert(
324
+ uatResultFilePath !== null,
325
+ 'resolveSliceFile(..., "UAT-RESULT") returns non-null when result file exists (idempotent skip state)',
326
+ );
327
+ } finally {
328
+ cleanup(base);
329
+ }
330
+ }
331
+
332
+ // ═══════════════════════════════════════════════════════════════════════════
333
+ // Results
334
+ // ═══════════════════════════════════════════════════════════════════════════
335
+
336
+ console.log(`\n${'='.repeat(40)}`);
337
+ console.log(`Results: ${passed} passed, ${failed} failed`);
338
+ if (failed > 0) {
339
+ process.exit(1);
340
+ } else {
341
+ console.log('All tests passed ✓');
342
+ }
343
+ }
344
+
345
+ main().catch((error) => {
346
+ console.error(error);
347
+ process.exit(1);
348
+ });
@@ -0,0 +1,247 @@
1
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import {
5
+ clearUnitRuntimeRecord,
6
+ formatExecuteTaskRecoveryStatus,
7
+ inspectExecuteTaskDurability,
8
+ readUnitRuntimeRecord,
9
+ writeUnitRuntimeRecord,
10
+ } from "../unit-runtime.ts";
11
+
12
+ let passed = 0;
13
+ let failed = 0;
14
+
15
+ function assert(condition: boolean, message: string): void {
16
+ if (condition) 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)) passed++;
25
+ else {
26
+ failed++;
27
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
28
+ }
29
+ }
30
+
31
+ const base = mkdtempSync(join(tmpdir(), "gsd-unit-runtime-test-"));
32
+ const tasksDir = join(base, ".gsd", "milestones", "M100", "slices", "S02", "tasks");
33
+ mkdirSync(tasksDir, { recursive: true });
34
+ writeFileSync(join(base, ".gsd", "STATE.md"), "## Next Action\nExecute T09 for S02: do the thing\n", "utf-8");
35
+ writeFileSync(
36
+ join(base, ".gsd", "milestones", "M100", "slices", "S02", "S02-PLAN.md"),
37
+ "# S02: Test Slice\n\n## Tasks\n\n- [ ] **T09: Do the thing** `est:10m`\n Description.\n",
38
+ "utf-8",
39
+ );
40
+
41
+ console.log("\n=== runtime record write/read/update ===");
42
+ {
43
+ const first = writeUnitRuntimeRecord(base, "execute-task", "M100/S02/T09", 1000, { phase: "dispatched" });
44
+ assertEq(first.phase, "dispatched", "initial phase");
45
+ const second = writeUnitRuntimeRecord(base, "execute-task", "M100/S02/T09", 1000, { phase: "wrapup-warning-sent", wrapupWarningSent: true });
46
+ assertEq(second.wrapupWarningSent, true, "warning persisted");
47
+ const loaded = readUnitRuntimeRecord(base, "execute-task", "M100/S02/T09");
48
+ assert(loaded !== null, "record readable");
49
+ assertEq(loaded!.phase, "wrapup-warning-sent", "updated phase readable");
50
+ }
51
+
52
+ console.log("\n=== execute-task durability inspection ===");
53
+ {
54
+ let status = await inspectExecuteTaskDurability(base, "M100/S02/T09");
55
+ assert(status !== null, "status exists");
56
+ assertEq(status!.summaryExists, false, "summary initially missing");
57
+ assertEq(status!.taskChecked, false, "task initially unchecked");
58
+ assertEq(status!.nextActionAdvanced, false, "next action initially stale");
59
+ assert(/summary missing/i.test(formatExecuteTaskRecoveryStatus(status!)), "diagnostic mentions summary");
60
+
61
+ writeFileSync(join(tasksDir, "T09-SUMMARY.md"), "# done\n", "utf-8");
62
+ writeFileSync(
63
+ join(base, ".gsd", "milestones", "M100", "slices", "S02", "S02-PLAN.md"),
64
+ "# S02: Test Slice\n\n## Tasks\n\n- [x] **T09: Do the thing** `est:10m`\n Description.\n",
65
+ "utf-8",
66
+ );
67
+ writeFileSync(join(base, ".gsd", "STATE.md"), "## Next Action\nExecute T10 for S02: next thing\n", "utf-8");
68
+
69
+ status = await inspectExecuteTaskDurability(base, "M100/S02/T09");
70
+ assertEq(status!.summaryExists, true, "summary found after write");
71
+ assertEq(status!.taskChecked, true, "task checked after update");
72
+ assertEq(status!.nextActionAdvanced, true, "next action advanced after update");
73
+ assertEq(formatExecuteTaskRecoveryStatus(status!), "all durable task artifacts present", "clean diagnostic when complete");
74
+ }
75
+
76
+ console.log("\n=== runtime record cleanup ===");
77
+ {
78
+ clearUnitRuntimeRecord(base, "execute-task", "M100/S02/T09");
79
+ const loaded = readUnitRuntimeRecord(base, "execute-task", "M100/S02/T09");
80
+ assertEq(loaded, null, "record removed");
81
+ }
82
+
83
+ // ─── Must-have durability integration tests ───────────────────────────────
84
+
85
+ // Create a separate temp base for must-have tests to avoid interference
86
+ const mhBase = mkdtempSync(join(tmpdir(), "gsd-unit-runtime-mh-test-"));
87
+
88
+ console.log("\n=== must-haves: all mentioned in summary ===");
89
+ {
90
+ const tasksDir2 = join(mhBase, ".gsd", "milestones", "M200", "slices", "S01", "tasks");
91
+ mkdirSync(tasksDir2, { recursive: true });
92
+
93
+ // Slice plan with T01 checked
94
+ writeFileSync(
95
+ join(mhBase, ".gsd", "milestones", "M200", "slices", "S01", "S01-PLAN.md"),
96
+ "# S01: Test\n\n## Tasks\n\n- [x] **T01: Build parser** `est:10m`\n Build the parser.\n",
97
+ "utf-8",
98
+ );
99
+ // Task plan with must-haves containing backtick code tokens
100
+ writeFileSync(
101
+ join(tasksDir2, "T01-PLAN.md"),
102
+ "# T01: Build parser\n\n## Must-Haves\n\n- [ ] `parseWidget` function is exported\n- [ ] `formatWidget` handles edge cases\n- [ ] All existing tests pass\n\n## Steps\n\n1. Do stuff\n",
103
+ "utf-8",
104
+ );
105
+ // Summary that mentions all must-haves
106
+ writeFileSync(
107
+ join(tasksDir2, "T01-SUMMARY.md"),
108
+ "# T01: Build parser\n\nAdded parseWidget function and formatWidget with edge case handling. All existing tests pass without regression.\n",
109
+ "utf-8",
110
+ );
111
+ // STATE.md with next action advanced past T01
112
+ writeFileSync(join(mhBase, ".gsd", "STATE.md"), "## Next Action\nExecute T02 for S01: next thing\n", "utf-8");
113
+
114
+ const status = await inspectExecuteTaskDurability(mhBase, "M200/S01/T01");
115
+ assert(status !== null, "mh-all: status exists");
116
+ assertEq(status!.mustHaveCount, 3, "mh-all: mustHaveCount is 3");
117
+ assertEq(status!.mustHavesMentionedInSummary, 3, "mh-all: all 3 must-haves mentioned");
118
+ assertEq(status!.summaryExists, true, "mh-all: summary exists");
119
+ assertEq(status!.taskChecked, true, "mh-all: task checked");
120
+ const diag = formatExecuteTaskRecoveryStatus(status!);
121
+ assertEq(diag, "all durable task artifacts present", "mh-all: diagnostic is clean when all must-haves met");
122
+ }
123
+
124
+ console.log("\n=== must-haves: partially mentioned in summary ===");
125
+ {
126
+ const tasksDir3 = join(mhBase, ".gsd", "milestones", "M200", "slices", "S02", "tasks");
127
+ mkdirSync(tasksDir3, { recursive: true });
128
+
129
+ writeFileSync(
130
+ join(mhBase, ".gsd", "milestones", "M200", "slices", "S02", "S02-PLAN.md"),
131
+ "# S02: Test\n\n## Tasks\n\n- [x] **T01: Build thing** `est:10m`\n Build.\n",
132
+ "utf-8",
133
+ );
134
+ // Task plan with 3 must-haves, summary will only mention 1
135
+ writeFileSync(
136
+ join(tasksDir3, "T01-PLAN.md"),
137
+ "# T01: Build thing\n\n## Must-Haves\n\n- [ ] `computeScore` function is exported\n- [ ] `validateInput` rejects invalid data\n- [ ] `renderOutput` handles empty arrays\n\n## Steps\n\n1. Do stuff\n",
138
+ "utf-8",
139
+ );
140
+ // Summary only mentions computeScore
141
+ writeFileSync(
142
+ join(tasksDir3, "T01-SUMMARY.md"),
143
+ "# T01: Build thing\n\nAdded computeScore function with full test coverage.\n",
144
+ "utf-8",
145
+ );
146
+ writeFileSync(join(mhBase, ".gsd", "STATE.md"), "## Next Action\nExecute T02 for S02: next thing\n", "utf-8");
147
+
148
+ const status = await inspectExecuteTaskDurability(mhBase, "M200/S02/T01");
149
+ assert(status !== null, "mh-partial: status exists");
150
+ assertEq(status!.mustHaveCount, 3, "mh-partial: mustHaveCount is 3");
151
+ assertEq(status!.mustHavesMentionedInSummary, 1, "mh-partial: only 1 must-have mentioned");
152
+ const diag = formatExecuteTaskRecoveryStatus(status!);
153
+ assert(diag.includes("must-have gap"), "mh-partial: diagnostic includes 'must-have gap'");
154
+ assert(diag.includes("1 of 3"), "mh-partial: diagnostic includes '1 of 3'");
155
+ }
156
+
157
+ console.log("\n=== must-haves: no task plan file ===");
158
+ {
159
+ const tasksDir4 = join(mhBase, ".gsd", "milestones", "M200", "slices", "S03", "tasks");
160
+ mkdirSync(tasksDir4, { recursive: true });
161
+
162
+ writeFileSync(
163
+ join(mhBase, ".gsd", "milestones", "M200", "slices", "S03", "S03-PLAN.md"),
164
+ "# S03: Test\n\n## Tasks\n\n- [x] **T01: Quick fix** `est:5m`\n Fix.\n",
165
+ "utf-8",
166
+ );
167
+ // No T01-PLAN.md — only summary
168
+ writeFileSync(
169
+ join(tasksDir4, "T01-SUMMARY.md"),
170
+ "# T01: Quick fix\n\nFixed the thing.\n",
171
+ "utf-8",
172
+ );
173
+ writeFileSync(join(mhBase, ".gsd", "STATE.md"), "## Next Action\nExecute T02 for S03: next thing\n", "utf-8");
174
+
175
+ const status = await inspectExecuteTaskDurability(mhBase, "M200/S03/T01");
176
+ assert(status !== null, "mh-noplan: status exists");
177
+ assertEq(status!.mustHaveCount, 0, "mh-noplan: mustHaveCount is 0 when no task plan");
178
+ assertEq(status!.mustHavesMentionedInSummary, 0, "mh-noplan: mustHavesMentionedInSummary is 0");
179
+ }
180
+
181
+ console.log("\n=== must-haves: present but no summary file ===");
182
+ {
183
+ const tasksDir5 = join(mhBase, ".gsd", "milestones", "M200", "slices", "S04", "tasks");
184
+ mkdirSync(tasksDir5, { recursive: true });
185
+
186
+ writeFileSync(
187
+ join(mhBase, ".gsd", "milestones", "M200", "slices", "S04", "S04-PLAN.md"),
188
+ "# S04: Test\n\n## Tasks\n\n- [ ] **T01: Build parser** `est:10m`\n Build.\n",
189
+ "utf-8",
190
+ );
191
+ // Task plan with must-haves but NO summary file
192
+ writeFileSync(
193
+ join(tasksDir5, "T01-PLAN.md"),
194
+ "# T01: Build parser\n\n## Must-Haves\n\n- [ ] `parseData` function exported\n- [ ] Error handling covers edge cases\n\n## Steps\n\n1. Do stuff\n",
195
+ "utf-8",
196
+ );
197
+ writeFileSync(join(mhBase, ".gsd", "STATE.md"), "## Next Action\nExecute T01 for S04: build parser\n", "utf-8");
198
+
199
+ const status = await inspectExecuteTaskDurability(mhBase, "M200/S04/T01");
200
+ assert(status !== null, "mh-nosummary: status exists");
201
+ assertEq(status!.mustHaveCount, 2, "mh-nosummary: mustHaveCount is 2");
202
+ assertEq(status!.mustHavesMentionedInSummary, 0, "mh-nosummary: mustHavesMentionedInSummary is 0 with no summary");
203
+ assertEq(status!.summaryExists, false, "mh-nosummary: summary doesn't exist");
204
+ }
205
+
206
+ console.log("\n=== must-haves: substring matching (no backtick tokens) ===");
207
+ {
208
+ const tasksDir6 = join(mhBase, ".gsd", "milestones", "M200", "slices", "S05", "tasks");
209
+ mkdirSync(tasksDir6, { recursive: true });
210
+
211
+ writeFileSync(
212
+ join(mhBase, ".gsd", "milestones", "M200", "slices", "S05", "S05-PLAN.md"),
213
+ "# S05: Test\n\n## Tasks\n\n- [x] **T01: Add diagnostics** `est:10m`\n Add.\n",
214
+ "utf-8",
215
+ );
216
+ // Must-haves with no backtick tokens — falls back to substring matching
217
+ writeFileSync(
218
+ join(tasksDir6, "T01-PLAN.md"),
219
+ "# T01: Add diagnostics\n\n## Must-Haves\n\n- [ ] Heuristic matching prioritizes backtick-enclosed code tokens\n- [ ] Recovery diagnostic string shows gap count\n- [ ] All assertions pass\n\n## Steps\n\n1. Do stuff\n",
220
+ "utf-8",
221
+ );
222
+ // Summary mentions "heuristic" and "diagnostic" but not "assertions"
223
+ writeFileSync(
224
+ join(tasksDir6, "T01-SUMMARY.md"),
225
+ "# T01: Add diagnostics\n\nImplemented heuristic matching for must-have items. Recovery diagnostic string now includes gap counts.\n",
226
+ "utf-8",
227
+ );
228
+ writeFileSync(join(mhBase, ".gsd", "STATE.md"), "## Next Action\nExecute T02 for S05: next thing\n", "utf-8");
229
+
230
+ const status = await inspectExecuteTaskDurability(mhBase, "M200/S05/T01");
231
+ assert(status !== null, "mh-substr: status exists");
232
+ assertEq(status!.mustHaveCount, 3, "mh-substr: mustHaveCount is 3");
233
+ // "heuristic" appears in summary for item 1, "diagnostic" for item 2,
234
+ // "assertions" appears in summary? No — let's check
235
+ // Item 3: "All assertions pass" — words: "assertions", "pass" (<4 chars excluded)
236
+ // summary doesn't contain "assertions" → not matched
237
+ assertEq(status!.mustHavesMentionedInSummary, 2, "mh-substr: 2 of 3 matched via substring");
238
+ const diag = formatExecuteTaskRecoveryStatus(status!);
239
+ assert(diag.includes("must-have gap"), "mh-substr: diagnostic includes gap info");
240
+ assert(diag.includes("2 of 3"), "mh-substr: diagnostic includes '2 of 3'");
241
+ }
242
+
243
+ rmSync(mhBase, { recursive: true, force: true });
244
+ rmSync(base, { recursive: true, force: true });
245
+ console.log(`\nResults: ${passed} passed, ${failed} failed`);
246
+ if (failed > 0) process.exit(1);
247
+ console.log("All tests passed ✓");
@@ -0,0 +1,53 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { resolveWorkflowConfig } from '../preferences.ts';
4
+
5
+ test('resolveWorkflowConfig returns all required keys', () => {
6
+ const wf = resolveWorkflowConfig();
7
+ const keys = Object.keys(wf).sort();
8
+ assert.deepEqual(keys, [
9
+ 'skip_milestone_research',
10
+ 'skip_observability',
11
+ 'skip_plan_self_audit',
12
+ 'skip_reassessment',
13
+ 'skip_slice_research',
14
+ ]);
15
+ });
16
+
17
+ test('resolveWorkflowConfig values are all booleans', () => {
18
+ const wf = resolveWorkflowConfig();
19
+ for (const [key, value] of Object.entries(wf)) {
20
+ assert.equal(typeof value, 'boolean', `${key} should be boolean, got ${typeof value}`);
21
+ }
22
+ });
23
+
24
+ // With ~/.gsd/preferences.md having planning_depth: standard and
25
+ // workflow overrides, verify the merge logic works:
26
+ // - planning_depth: standard sets skip_slice_research, skip_plan_self_audit to true (NOT milestone research)
27
+ // - explicit workflow.skip_milestone_research: false confirms standard default (false)
28
+ // - explicit workflow.skip_slice_research: true confirms standard default (true)
29
+ // - explicit workflow.skip_plan_self_audit: true confirms standard default (true)
30
+ // - explicit workflow.skip_reassessment: false confirms standard default (false)
31
+ test('resolveWorkflowConfig respects global preferences with overrides', () => {
32
+ const wf = resolveWorkflowConfig();
33
+
34
+ // planning_depth: standard → skip_milestone_research defaults false,
35
+ // workflow.skip_milestone_research: false confirms it
36
+ assert.equal(wf.skip_milestone_research, false);
37
+
38
+ // planning_depth: standard → skip_slice_research defaults true,
39
+ // workflow.skip_slice_research: true confirms it
40
+ assert.equal(wf.skip_slice_research, true);
41
+
42
+ // planning_depth: standard → skip_plan_self_audit defaults true,
43
+ // workflow.skip_plan_self_audit: true confirms it
44
+ assert.equal(wf.skip_plan_self_audit, true);
45
+
46
+ // planning_depth: standard → skip_reassessment defaults false,
47
+ // workflow.skip_reassessment: false confirms it
48
+ assert.equal(wf.skip_reassessment, false);
49
+
50
+ // planning_depth: standard → skip_observability defaults false,
51
+ // no explicit override, uses standard default (false)
52
+ assert.equal(wf.skip_observability, false);
53
+ });