gsd-pi 2.38.0-dev.96dc7fb → 2.38.0-dev.98b44dc

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 (217) hide show
  1. package/README.md +15 -11
  2. package/dist/app-paths.js +1 -1
  3. package/dist/extension-registry.js +2 -2
  4. package/dist/remote-questions-config.js +2 -2
  5. package/dist/resource-loader.js +34 -1
  6. package/dist/resources/extensions/browser-tools/index.js +3 -1
  7. package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
  8. package/dist/resources/extensions/env-utils.js +29 -0
  9. package/dist/resources/extensions/get-secrets-from-user.js +5 -24
  10. package/dist/resources/extensions/github-sync/cli.js +284 -0
  11. package/dist/resources/extensions/github-sync/index.js +73 -0
  12. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  13. package/dist/resources/extensions/github-sync/sync.js +424 -0
  14. package/dist/resources/extensions/github-sync/templates.js +118 -0
  15. package/dist/resources/extensions/github-sync/types.js +7 -0
  16. package/dist/resources/extensions/gsd/auto/session.js +6 -23
  17. package/dist/resources/extensions/gsd/auto-dispatch.js +8 -9
  18. package/dist/resources/extensions/gsd/auto-loop.js +636 -594
  19. package/dist/resources/extensions/gsd/auto-post-unit.js +99 -70
  20. package/dist/resources/extensions/gsd/auto-prompts.js +202 -48
  21. package/dist/resources/extensions/gsd/auto-start.js +7 -1
  22. package/dist/resources/extensions/gsd/auto-worktree-sync.js +2 -1
  23. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  24. package/dist/resources/extensions/gsd/auto.js +143 -96
  25. package/dist/resources/extensions/gsd/commands-extensions.js +3 -2
  26. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +1 -1
  27. package/dist/resources/extensions/gsd/commands.js +4 -2
  28. package/dist/resources/extensions/gsd/context-budget.js +2 -10
  29. package/dist/resources/extensions/gsd/detection.js +1 -2
  30. package/dist/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  31. package/dist/resources/extensions/gsd/doctor-providers.js +30 -11
  32. package/dist/resources/extensions/gsd/doctor.js +20 -1
  33. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  34. package/dist/resources/extensions/gsd/export.js +1 -1
  35. package/dist/resources/extensions/gsd/files.js +48 -9
  36. package/dist/resources/extensions/gsd/forensics.js +1 -1
  37. package/dist/resources/extensions/gsd/git-service.js +30 -12
  38. package/dist/resources/extensions/gsd/gitignore.js +16 -3
  39. package/dist/resources/extensions/gsd/guided-flow.js +149 -38
  40. package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
  41. package/dist/resources/extensions/gsd/health-widget.js +3 -86
  42. package/dist/resources/extensions/gsd/index.js +24 -20
  43. package/dist/resources/extensions/gsd/migrate/parsers.js +1 -1
  44. package/dist/resources/extensions/gsd/migrate-external.js +18 -1
  45. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  46. package/dist/resources/extensions/gsd/paths.js +3 -0
  47. package/dist/resources/extensions/gsd/preferences-models.js +0 -12
  48. package/dist/resources/extensions/gsd/preferences-types.js +1 -1
  49. package/dist/resources/extensions/gsd/preferences-validation.js +59 -11
  50. package/dist/resources/extensions/gsd/preferences.js +22 -11
  51. package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
  52. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  53. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  54. package/dist/resources/extensions/gsd/prompts/discuss.md +11 -14
  55. package/dist/resources/extensions/gsd/prompts/execute-task.md +5 -3
  56. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  57. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  58. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  59. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  60. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  61. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  62. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  63. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  64. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  65. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  66. package/dist/resources/extensions/gsd/prompts/queue.md +4 -8
  67. package/dist/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  68. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  69. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  70. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  71. package/dist/resources/extensions/gsd/prompts/run-uat.md +28 -11
  72. package/dist/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  73. package/dist/resources/extensions/gsd/repo-identity.js +21 -4
  74. package/dist/resources/extensions/gsd/resource-version.js +2 -1
  75. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  76. package/dist/resources/extensions/gsd/state.js +42 -23
  77. package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
  78. package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
  79. package/dist/resources/extensions/gsd/visualizer-data.js +1 -1
  80. package/dist/resources/extensions/mcp-client/index.js +14 -1
  81. package/dist/resources/extensions/remote-questions/status.js +4 -1
  82. package/dist/resources/extensions/remote-questions/store.js +4 -1
  83. package/dist/resources/extensions/search-the-web/provider.js +2 -1
  84. package/dist/resources/extensions/shared/frontmatter.js +1 -1
  85. package/dist/resources/extensions/subagent/isolation.js +2 -1
  86. package/dist/resources/extensions/ttsr/rule-loader.js +2 -1
  87. package/package.json +1 -1
  88. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  89. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  90. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  91. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  92. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  93. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  94. package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
  95. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/skills.js +6 -1
  97. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  98. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  99. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  100. package/packages/pi-coding-agent/dist/index.js +1 -1
  101. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  102. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  103. package/packages/pi-coding-agent/src/core/skills.ts +9 -1
  104. package/packages/pi-coding-agent/src/index.ts +1 -0
  105. package/src/resources/extensions/browser-tools/index.ts +3 -0
  106. package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
  107. package/src/resources/extensions/env-utils.ts +31 -0
  108. package/src/resources/extensions/get-secrets-from-user.ts +5 -24
  109. package/src/resources/extensions/github-sync/cli.ts +364 -0
  110. package/src/resources/extensions/github-sync/index.ts +93 -0
  111. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  112. package/src/resources/extensions/github-sync/sync.ts +556 -0
  113. package/src/resources/extensions/github-sync/templates.ts +183 -0
  114. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  115. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  116. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  117. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  118. package/src/resources/extensions/github-sync/types.ts +47 -0
  119. package/src/resources/extensions/gsd/auto/session.ts +7 -25
  120. package/src/resources/extensions/gsd/auto-dispatch.ts +7 -9
  121. package/src/resources/extensions/gsd/auto-loop.ts +526 -545
  122. package/src/resources/extensions/gsd/auto-post-unit.ts +80 -44
  123. package/src/resources/extensions/gsd/auto-prompts.ts +247 -50
  124. package/src/resources/extensions/gsd/auto-start.ts +11 -1
  125. package/src/resources/extensions/gsd/auto-worktree-sync.ts +3 -1
  126. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  127. package/src/resources/extensions/gsd/auto.ts +139 -101
  128. package/src/resources/extensions/gsd/commands-extensions.ts +4 -2
  129. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +1 -1
  130. package/src/resources/extensions/gsd/commands.ts +5 -3
  131. package/src/resources/extensions/gsd/context-budget.ts +2 -12
  132. package/src/resources/extensions/gsd/detection.ts +2 -2
  133. package/src/resources/extensions/gsd/docs/preferences-reference.md +0 -2
  134. package/src/resources/extensions/gsd/doctor-providers.ts +30 -9
  135. package/src/resources/extensions/gsd/doctor.ts +22 -1
  136. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  137. package/src/resources/extensions/gsd/export.ts +1 -1
  138. package/src/resources/extensions/gsd/files.ts +51 -11
  139. package/src/resources/extensions/gsd/forensics.ts +1 -1
  140. package/src/resources/extensions/gsd/git-service.ts +44 -10
  141. package/src/resources/extensions/gsd/gitignore.ts +17 -3
  142. package/src/resources/extensions/gsd/guided-flow.ts +177 -44
  143. package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
  144. package/src/resources/extensions/gsd/health-widget.ts +3 -89
  145. package/src/resources/extensions/gsd/index.ts +24 -17
  146. package/src/resources/extensions/gsd/migrate/parsers.ts +1 -1
  147. package/src/resources/extensions/gsd/migrate-external.ts +18 -1
  148. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  149. package/src/resources/extensions/gsd/paths.ts +4 -0
  150. package/src/resources/extensions/gsd/preferences-models.ts +0 -12
  151. package/src/resources/extensions/gsd/preferences-types.ts +4 -4
  152. package/src/resources/extensions/gsd/preferences-validation.ts +51 -11
  153. package/src/resources/extensions/gsd/preferences.ts +25 -11
  154. package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
  155. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  156. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  157. package/src/resources/extensions/gsd/prompts/discuss.md +11 -14
  158. package/src/resources/extensions/gsd/prompts/execute-task.md +5 -3
  159. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  160. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +11 -12
  161. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +8 -10
  162. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  163. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  164. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  165. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  166. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  167. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  168. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  169. package/src/resources/extensions/gsd/prompts/queue.md +4 -8
  170. package/src/resources/extensions/gsd/prompts/reactive-execute.md +11 -8
  171. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  172. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  173. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  174. package/src/resources/extensions/gsd/prompts/run-uat.md +28 -11
  175. package/src/resources/extensions/gsd/prompts/workflow-start.md +2 -2
  176. package/src/resources/extensions/gsd/repo-identity.ts +23 -4
  177. package/src/resources/extensions/gsd/resource-version.ts +3 -1
  178. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  179. package/src/resources/extensions/gsd/state.ts +39 -21
  180. package/src/resources/extensions/gsd/templates/runtime.md +21 -0
  181. package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
  182. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +21 -18
  183. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +122 -68
  184. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
  185. package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
  186. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +86 -3
  187. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
  188. package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
  189. package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
  190. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
  191. package/src/resources/extensions/gsd/tests/preferences.test.ts +2 -7
  192. package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +59 -0
  193. package/src/resources/extensions/gsd/tests/repo-identity-worktree.test.ts +21 -1
  194. package/src/resources/extensions/gsd/tests/run-uat.test.ts +16 -4
  195. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
  196. package/src/resources/extensions/gsd/types.ts +18 -1
  197. package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
  198. package/src/resources/extensions/gsd/visualizer-data.ts +1 -1
  199. package/src/resources/extensions/mcp-client/index.ts +17 -1
  200. package/src/resources/extensions/remote-questions/status.ts +5 -1
  201. package/src/resources/extensions/remote-questions/store.ts +5 -1
  202. package/src/resources/extensions/search-the-web/provider.ts +2 -1
  203. package/src/resources/extensions/shared/frontmatter.ts +1 -1
  204. package/src/resources/extensions/subagent/isolation.ts +3 -1
  205. package/src/resources/extensions/ttsr/rule-loader.ts +3 -1
  206. package/dist/resources/extensions/gsd/prompt-compressor.js +0 -393
  207. package/dist/resources/extensions/gsd/semantic-chunker.js +0 -254
  208. package/dist/resources/extensions/gsd/summary-distiller.js +0 -212
  209. package/src/resources/extensions/gsd/prompt-compressor.ts +0 -508
  210. package/src/resources/extensions/gsd/semantic-chunker.ts +0 -336
  211. package/src/resources/extensions/gsd/summary-distiller.ts +0 -258
  212. package/src/resources/extensions/gsd/tests/context-compression.test.ts +0 -193
  213. package/src/resources/extensions/gsd/tests/prompt-compressor.test.ts +0 -529
  214. package/src/resources/extensions/gsd/tests/semantic-chunker.test.ts +0 -426
  215. package/src/resources/extensions/gsd/tests/summary-distiller.test.ts +0 -323
  216. package/src/resources/extensions/gsd/tests/token-optimization-benchmark.test.ts +0 -1272
  217. package/src/resources/extensions/gsd/tests/token-optimization-prefs.test.ts +0 -164
@@ -80,66 +80,28 @@ test("buildHealthLines: initialized state shows continue setup copy", () => {
80
80
  ]);
81
81
  });
82
82
 
83
- test("buildHealthLines: active state leads with execution summary", () => {
84
- const lines = buildHealthLines(activeData({
85
- executionStatus: "Executing",
86
- executionTarget: "Plan S01",
87
- progress: {
88
- milestones: { done: 0, total: 1 },
89
- slices: { done: 0, total: 3 },
90
- tasks: { done: 0, total: 5 },
91
- },
92
- }));
93
-
94
- assert.equal(lines.length, 2);
95
- assert.equal(lines[0], " GSD Executing - Plan S01");
96
- assert.match(lines[1]!, /Progress: M 0\/1 · S 0\/3 · T 0\/5/);
97
- });
98
-
99
- test("buildHealthLines: active state keeps issues secondary", () => {
100
- const lines = buildHealthLines(activeData({
101
- executionStatus: "Planning",
102
- executionTarget: "Execute T03",
103
- providerIssue: "✗ Anthropic (Claude) key missing",
104
- environmentWarningCount: 1,
105
- budgetSpent: 0.42,
106
- }));
107
-
108
- assert.equal(lines.length, 2);
109
- assert.equal(lines[0], " GSD Planning - Execute T03");
110
- assert.match(lines[1]!, /✗ Anthropic \(Claude\) key missing/);
111
- assert.match(lines[1]!, /Env: 1 warning/);
112
- assert.match(lines[1]!, /Spent: 42\.0¢/);
113
- });
114
-
115
- test("buildHealthLines: blocked state explains wait reason", () => {
116
- const lines = buildHealthLines(activeData({
117
- executionStatus: "Blocked",
118
- executionTarget: "waiting on unmet deps: M001",
119
- blocker: "M002 is waiting on unmet deps: M001",
120
- }));
121
-
122
- assert.equal(lines[0], " GSD Blocked - waiting on unmet deps: M001");
83
+ test("buildHealthLines: active state with ledger-driven spend shows spent summary", () => {
84
+ const lines = buildHealthLines(activeData({ budgetSpent: 0.42 }));
85
+ assert.equal(lines.length, 1);
86
+ assert.match(lines[0]!, /● System OK/);
87
+ assert.match(lines[0]!, /Spent: 42\.0¢/);
123
88
  });
124
89
 
125
- test("buildHealthLines: paused state can omit secondary line", () => {
126
- const lines = buildHealthLines(activeData({
127
- executionStatus: "Paused",
128
- executionTarget: "waiting to resume",
129
- }));
130
-
131
- assert.deepEqual(lines, [" GSD Paused - waiting to resume"]);
90
+ test("buildHealthLines: active state with budget ceiling shows percent summary", () => {
91
+ const lines = buildHealthLines(activeData({ budgetSpent: 2.5, budgetCeiling: 10 }));
92
+ assert.equal(lines.length, 1);
93
+ assert.match(lines[0]!, /Budget: \$2\.50\/\$10\.00 \(25%\)/);
132
94
  });
133
95
 
134
- test("buildHealthLines: active state with budget ceiling shows percent summary", () => {
96
+ test("buildHealthLines: active state with issues reports issue summary", () => {
135
97
  const lines = buildHealthLines(activeData({
136
- executionStatus: "Executing",
137
- executionTarget: "Plan S01",
138
- budgetSpent: 2.5,
139
- budgetCeiling: 10,
98
+ providerIssue: "✗ OpenAI key missing",
99
+ environmentErrorCount: 1,
140
100
  }));
141
- assert.equal(lines.length, 2);
142
- assert.match(lines[1]!, /Budget: \$2\.50\/\$10\.00 \(25%\)/);
101
+ assert.equal(lines.length, 1);
102
+ assert.match(lines[0]!, /✗ 2 issues/);
103
+ assert.match(lines[0]!, /✗ OpenAI key missing/);
104
+ assert.match(lines[0]!, /Env: 1 error/);
143
105
  });
144
106
 
145
107
  test("detectHealthWidgetProjectState: metrics file alone does not imply project", () => {
@@ -1,4 +1,4 @@
1
- import { parseRoadmap, parsePlan, parseSummary, parseContinue, parseRequirementCounts, parseSecretsManifest, formatSecretsManifest } from '../files.ts';
1
+ import { parseRoadmap, parsePlan, parseTaskPlanFile, parseSummary, parseContinue, parseRequirementCounts, parseSecretsManifest, formatSecretsManifest } from '../files.ts';
2
2
  import { createTestContext } from './test-helpers.ts';
3
3
 
4
4
  const { assertEq, assertTrue, report } = createTestContext();
@@ -241,7 +241,15 @@ console.log('\n=== parseRoadmap: missing risk defaults to low ===');
241
241
 
242
242
  console.log('\n=== parsePlan: full plan ===');
243
243
  {
244
- const content = `# S01: Parser Test Suite
244
+ const content = `---
245
+ estimated_steps: 6
246
+ estimated_files: 3
247
+ skills_used:
248
+ - typescript
249
+ - testing
250
+ ---
251
+
252
+ # S01: Parser Test Suite
245
253
 
246
254
  **Goal:** All 5 parsers have test coverage with edge cases.
247
255
  **Demo:** \`node --test tests/parsers.test.ts\` passes with zero failures.
@@ -267,6 +275,13 @@ console.log('\n=== parsePlan: full plan ===');
267
275
  - \`files.ts\` — update parseSummary
268
276
  `;
269
277
 
278
+ const taskPlan = parseTaskPlanFile(content);
279
+ assertEq(taskPlan.frontmatter.estimated_steps, 6, 'task plan frontmatter estimated_steps');
280
+ assertEq(taskPlan.frontmatter.estimated_files, 3, 'task plan frontmatter estimated_files');
281
+ assertEq(taskPlan.frontmatter.skills_used.length, 2, 'task plan frontmatter skills_used count');
282
+ assertEq(taskPlan.frontmatter.skills_used[0], 'typescript', 'first task plan skill');
283
+ assertEq(taskPlan.frontmatter.skills_used[1], 'testing', 'second task plan skill');
284
+
270
285
  const p = parsePlan(content);
271
286
 
272
287
  assertEq(p.id, 'S01', 'plan id');
@@ -295,6 +310,97 @@ console.log('\n=== parsePlan: full plan ===');
295
310
  assertTrue(p.filesLikelyTouched[0].includes('tests/parsers.test.ts'), 'first file');
296
311
  }
297
312
 
313
+ console.log('\n=== parseTaskPlanFile: defaults missing frontmatter fields ===');
314
+ {
315
+ const content = `# T01: Minimal task plan
316
+
317
+ ## Description
318
+
319
+ No frontmatter here.
320
+ `;
321
+
322
+ const taskPlan = parseTaskPlanFile(content);
323
+ assertEq(taskPlan.frontmatter.estimated_steps, undefined, 'estimated_steps defaults undefined');
324
+ assertEq(taskPlan.frontmatter.estimated_files, undefined, 'estimated_files defaults undefined');
325
+ assertEq(taskPlan.frontmatter.skills_used.length, 0, 'skills_used defaults empty array');
326
+ }
327
+
328
+ console.log('\n=== parseTaskPlanFile: accepts scalar skills_used and numeric strings ===');
329
+ {
330
+ const content = `---
331
+ estimated_steps: "9"
332
+ estimated_files: "4"
333
+ skills_used: react-best-practices
334
+ ---
335
+
336
+ # T02: Scalar skill handoff
337
+ `;
338
+
339
+ const taskPlan = parseTaskPlanFile(content);
340
+ assertEq(taskPlan.frontmatter.estimated_steps, 9, 'string estimated_steps parsed');
341
+ assertEq(taskPlan.frontmatter.estimated_files, 4, 'string estimated_files parsed');
342
+ assertEq(taskPlan.frontmatter.skills_used.length, 1, 'scalar skills_used normalized to array');
343
+ assertEq(taskPlan.frontmatter.skills_used[0], 'react-best-practices', 'scalar skill preserved');
344
+ }
345
+
346
+ console.log('\n=== parseTaskPlanFile: filters blank skills_used items ===');
347
+ {
348
+ const content = `---
349
+ skills_used:
350
+ - react
351
+ -
352
+ - testing
353
+ ---
354
+
355
+ # T03: Blank skills filtered
356
+ `;
357
+
358
+ const taskPlan = parseTaskPlanFile(content);
359
+ assertEq(taskPlan.frontmatter.skills_used.length, 2, 'blank skill entries removed');
360
+ assertEq(taskPlan.frontmatter.skills_used[0], 'react', 'first remaining skill');
361
+ assertEq(taskPlan.frontmatter.skills_used[1], 'testing', 'second remaining skill');
362
+ }
363
+
364
+ console.log('\n=== parseTaskPlanFile: invalid numeric frontmatter ignored ===');
365
+ {
366
+ const content = `---
367
+ estimated_steps: many
368
+ estimated_files: unknown
369
+ ---
370
+
371
+ # T04: Invalid estimates
372
+ `;
373
+
374
+ const taskPlan = parseTaskPlanFile(content);
375
+ assertEq(taskPlan.frontmatter.estimated_steps, undefined, 'invalid estimated_steps ignored');
376
+ assertEq(taskPlan.frontmatter.estimated_files, undefined, 'invalid estimated_files ignored');
377
+ }
378
+
379
+ console.log('\n=== parseTaskPlanFile: parsePlan ignores task-plan frontmatter ===');
380
+ {
381
+ const content = `---
382
+ estimated_steps: 2
383
+ estimated_files: 1
384
+ skills_used:
385
+ - react
386
+ ---
387
+
388
+ # S11: Frontmatter Compatible
389
+
390
+ **Goal:** Plan parser ignores task-plan handoff metadata.
391
+ **Demo:** Slice content still parses.
392
+
393
+ ## Tasks
394
+
395
+ - [ ] **T01: Compatible task** \`est:5m\`
396
+ Description.
397
+ `;
398
+
399
+ const p = parsePlan(content);
400
+ assertEq(p.id, 'S11', 'plan id still parsed with frontmatter');
401
+ assertEq(p.tasks.length, 1, 'task still parsed with frontmatter');
402
+ }
403
+
298
404
  console.log('\n=== parsePlan: multi-line task description concatenation ===');
299
405
  {
300
406
  const content = `# S02: Multi-line Test
@@ -324,16 +430,36 @@ console.log('\n=== parsePlan: multi-line task description concatenation ===');
324
430
  const p = parsePlan(content);
325
431
 
326
432
  assertEq(p.tasks.length, 2, 'two tasks');
327
- // Multi-line descriptions should be concatenated with spaces
328
433
  assertTrue(p.tasks[0].description.includes('First line'), 'T01 desc has first line');
329
434
  assertTrue(p.tasks[0].description.includes('Second line'), 'T01 desc has second line');
330
435
  assertTrue(p.tasks[0].description.includes('Third line'), 'T01 desc has third line');
331
- // Verify concatenation with space separator
332
436
  assertTrue(p.tasks[0].description.includes('description. Second'), 'lines joined with space');
333
-
334
437
  assertEq(p.tasks[1].description, 'Just one line.', 'T02 single-line desc');
335
438
  }
336
439
 
440
+ console.log('\n=== parsePlan: frontmatter does not pollute task descriptions ===');
441
+ {
442
+ const content = `---
443
+ estimated_steps: 2
444
+ estimated_files: 1
445
+ skills_used:
446
+ - react
447
+ ---
448
+
449
+ # S12: Frontmatter + multiline
450
+
451
+ ## Tasks
452
+
453
+ - [ ] **T01: Multi-line Task** \`est:30m\`
454
+ First line of description.
455
+ Second line of description.
456
+ `;
457
+
458
+ const p = parsePlan(content);
459
+ assertEq(p.tasks.length, 1, 'one task parsed with frontmatter');
460
+ assertEq(p.tasks[0].description, 'First line of description. Second line of description.', 'frontmatter excluded from description');
461
+ }
462
+
337
463
  console.log('\n=== parsePlan: task with missing estimate ===');
338
464
  {
339
465
  const content = `# S03: No Estimate
@@ -351,12 +477,10 @@ console.log('\n=== parsePlan: task with missing estimate ===');
351
477
  `;
352
478
 
353
479
  const p = parsePlan(content);
354
-
355
480
  assertEq(p.tasks.length, 2, 'two tasks parsed');
356
481
  assertEq(p.tasks[0].id, 'T01', 'T01 id');
357
482
  assertEq(p.tasks[0].title, 'No Estimate Task', 'T01 title without estimate');
358
483
  assertEq(p.tasks[0].done, false, 'T01 not done');
359
- // The estimate backtick text appears in description if present, but parser doesn't crash without it
360
484
  assertEq(p.tasks[1].id, 'T02', 'T02 id');
361
485
  }
362
486
 
@@ -379,7 +503,6 @@ console.log('\n=== parsePlan: empty tasks section ===');
379
503
  `;
380
504
 
381
505
  const p = parsePlan(content);
382
-
383
506
  assertEq(p.id, 'S04', 'plan id with empty tasks');
384
507
  assertEq(p.tasks.length, 0, 'no tasks');
385
508
  assertEq(p.mustHaves.length, 1, 'one must-have');
@@ -398,7 +521,6 @@ console.log('\n=== parsePlan: no H1 ===');
398
521
  `;
399
522
 
400
523
  const p = parsePlan(content);
401
-
402
524
  assertEq(p.id, '', 'empty id without H1');
403
525
  assertEq(p.title, '', 'empty title without H1');
404
526
  assertEq(p.goal, 'A plan without a heading.', 'goal still parsed');
@@ -408,8 +530,6 @@ console.log('\n=== parsePlan: no H1 ===');
408
530
 
409
531
  console.log('\n=== parsePlan: task estimate backtick in description ===');
410
532
  {
411
- // The `est:45m` text appears after the bold closing but before the description lines
412
- // It should end up as part of the description or be ignored gracefully
413
533
  const content = `# S05: Estimate Handling
414
534
 
415
535
  **Goal:** Test estimate text handling.
@@ -425,9 +545,6 @@ console.log('\n=== parsePlan: task estimate backtick in description ===');
425
545
  assertEq(p.tasks.length, 1, 'one task');
426
546
  assertEq(p.tasks[0].id, 'T01', 'task id');
427
547
  assertEq(p.tasks[0].title, 'With Estimate', 'title excludes estimate');
428
- // The `est:45m` backtick text after ** is not part of the title or description
429
- // It's on the same line after the regex match captures, so it's in the remainder
430
- // The description should be the continuation lines
431
548
  assertTrue(p.tasks[0].description.includes('Main description'), 'description from continuation line');
432
549
  }
433
550
 
@@ -26,8 +26,21 @@ const BASE_VARS = {
26
26
  inlinedContext: "--- test inlined context ---",
27
27
  dependencySummaries: "", executorContextConstraints: "",
28
28
  sourceFilePaths: "- **Requirements**: `.gsd/REQUIREMENTS.md`",
29
+ skillActivation: "Load the relevant skills.",
29
30
  };
30
31
 
32
+ const DEFAULT_SKILL_ACTIVATION = "If a `GSD Skill Preferences` block is present in system context, use it and the `<available_skills>` catalog in your system prompt to decide which skills to load and follow for this unit, without relaxing required verification or artifact rules.";
33
+
34
+ function loadPromptWithDefaultSkillActivation(name: string, vars: Record<string, string> = {}): string {
35
+ return loadPrompt(name, { skillActivation: DEFAULT_SKILL_ACTIVATION, ...vars });
36
+ }
37
+
38
+ function promptUsesSkillActivation(name: string): boolean {
39
+ const path = join(worktreePromptsDir, `${name}.md`);
40
+ const content = readFileSync(path, "utf-8");
41
+ return content.includes("{{skillActivation}}");
42
+ }
43
+
31
44
  test("plan-slice prompt: commit instruction says do not commit (external state)", () => {
32
45
  const result = loadPrompt("plan-slice", { ...BASE_VARS, commitInstruction: "Do not commit planning artifacts — .gsd/ is managed externally." });
33
46
  assert.ok(result.includes("Do not commit planning artifacts"));
@@ -40,3 +53,199 @@ test("plan-slice prompt: all variables substituted", () => {
40
53
  assert.ok(result.includes("M001"));
41
54
  assert.ok(result.includes("S01"));
42
55
  });
56
+
57
+ test("domain-work prompts use skillActivation placeholder", () => {
58
+ const prompts = [
59
+ "research-milestone",
60
+ "plan-milestone",
61
+ "research-slice",
62
+ "plan-slice",
63
+ "execute-task",
64
+ "guided-research-slice",
65
+ "guided-plan-milestone",
66
+ "guided-plan-slice",
67
+ "guided-execute-task",
68
+ "guided-resume-task",
69
+ ];
70
+
71
+ for (const name of prompts) {
72
+ assert.ok(promptUsesSkillActivation(name), `${name}.md should contain {{skillActivation}}`);
73
+ }
74
+ });
75
+
76
+ test("skillActivation default leaves no unresolved placeholder", () => {
77
+ const result = loadPromptWithDefaultSkillActivation("execute-task", {
78
+ workingDirectory: "/tmp/test-project",
79
+ milestoneId: "M001",
80
+ sliceId: "S01",
81
+ sliceTitle: "Test Slice",
82
+ taskId: "T01",
83
+ taskTitle: "Implement feature",
84
+ planPath: ".gsd/milestones/M001/slices/S01/S01-PLAN.md",
85
+ taskPlanPath: ".gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md",
86
+ taskPlanInline: "Task plan",
87
+ slicePlanExcerpt: "Slice excerpt",
88
+ carryForwardSection: "Carry forward",
89
+ resumeSection: "Resume",
90
+ priorTaskLines: "- (no prior tasks)",
91
+ taskSummaryPath: "/tmp/test-project/.gsd/milestones/M001/slices/S01/tasks/T01-SUMMARY.md",
92
+ inlinedTemplates: "Template",
93
+ verificationBudget: "~10K chars",
94
+ overridesSection: "",
95
+ });
96
+
97
+ assert.ok(!result.includes("{{skillActivation}}"));
98
+ assert.ok(result.includes(DEFAULT_SKILL_ACTIVATION));
99
+ });
100
+
101
+ test("custom skillActivation is substituted into execute-task", () => {
102
+ const result = loadPrompt("execute-task", {
103
+ workingDirectory: "/tmp/test-project",
104
+ milestoneId: "M001",
105
+ sliceId: "S01",
106
+ sliceTitle: "Test Slice",
107
+ taskId: "T01",
108
+ taskTitle: "Implement feature",
109
+ planPath: ".gsd/milestones/M001/slices/S01/S01-PLAN.md",
110
+ taskPlanPath: ".gsd/milestones/M001/slices/S01/tasks/T01-PLAN.md",
111
+ taskPlanInline: "Task plan",
112
+ slicePlanExcerpt: "Slice excerpt",
113
+ carryForwardSection: "Carry forward",
114
+ resumeSection: "Resume",
115
+ priorTaskLines: "- (no prior tasks)",
116
+ taskSummaryPath: "/tmp/test-project/.gsd/milestones/M001/slices/S01/tasks/T01-SUMMARY.md",
117
+ inlinedTemplates: "Template",
118
+ verificationBudget: "~10K chars",
119
+ overridesSection: "",
120
+ skillActivation: "Load React and accessibility skills first.",
121
+ });
122
+
123
+ assert.ok(result.includes("Load React and accessibility skills first."));
124
+ assert.ok(!result.includes("{{skillActivation}}"));
125
+ });
126
+
127
+ test("guided execute prompt substitutes skillActivation", () => {
128
+ const result = loadPrompt("guided-execute-task", {
129
+ milestoneId: "M001",
130
+ sliceId: "S01",
131
+ taskId: "T01",
132
+ taskTitle: "Implement feature",
133
+ inlinedTemplates: "Template",
134
+ skillActivation: "Load React skill first.",
135
+ });
136
+
137
+ assert.ok(result.includes("Load React skill first."));
138
+ assert.ok(!result.includes("{{skillActivation}}"));
139
+ });
140
+
141
+ test("guided resume prompt substitutes skillActivation", () => {
142
+ const result = loadPrompt("guided-resume-task", {
143
+ milestoneId: "M001",
144
+ sliceId: "S01",
145
+ skillActivation: "Load debugging skill first.",
146
+ });
147
+
148
+ assert.ok(result.includes("Load debugging skill first."));
149
+ assert.ok(!result.includes("{{skillActivation}}"));
150
+ });
151
+
152
+ test("research-milestone prompt substitutes skillActivation", () => {
153
+ const result = loadPrompt("research-milestone", {
154
+ workingDirectory: "/tmp/test-project",
155
+ milestoneId: "M001",
156
+ milestoneTitle: "Test Milestone",
157
+ milestonePath: ".gsd/milestones/M001",
158
+ contextPath: ".gsd/milestones/M001/M001-CONTEXT.md",
159
+ outputPath: "/tmp/test-project/.gsd/milestones/M001/M001-RESEARCH.md",
160
+ inlinedContext: "Context",
161
+ skillDiscoveryMode: "manual",
162
+ skillDiscoveryInstructions: " Discover skills manually.",
163
+ skillActivation: "Load research skills first.",
164
+ });
165
+
166
+ assert.ok(result.includes("Load research skills first."));
167
+ assert.ok(!result.includes("{{skillActivation}}"));
168
+ });
169
+
170
+ test("research-slice prompt substitutes skillActivation", () => {
171
+ const result = loadPrompt("research-slice", {
172
+ workingDirectory: "/tmp/test-project",
173
+ milestoneId: "M001",
174
+ sliceId: "S01",
175
+ sliceTitle: "Test Slice",
176
+ slicePath: ".gsd/milestones/M001/slices/S01",
177
+ roadmapPath: ".gsd/milestones/M001/M001-ROADMAP.md",
178
+ contextPath: ".gsd/milestones/M001/M001-CONTEXT.md",
179
+ milestoneResearchPath: ".gsd/milestones/M001/M001-RESEARCH.md",
180
+ outputPath: "/tmp/test-project/.gsd/milestones/M001/slices/S01/S01-RESEARCH.md",
181
+ inlinedContext: "Context",
182
+ dependencySummaries: "",
183
+ skillDiscoveryMode: "manual",
184
+ skillDiscoveryInstructions: " Discover skills manually.",
185
+ skillActivation: "Load slice research skills first.",
186
+ });
187
+
188
+ assert.ok(result.includes("Load slice research skills first."));
189
+ assert.ok(!result.includes("{{skillActivation}}"));
190
+ });
191
+
192
+ test("plan-milestone prompt substitutes skillActivation", () => {
193
+ const result = loadPrompt("plan-milestone", {
194
+ workingDirectory: "/tmp/test-project",
195
+ milestoneId: "M001",
196
+ milestoneTitle: "Test Milestone",
197
+ milestonePath: ".gsd/milestones/M001",
198
+ contextPath: ".gsd/milestones/M001/M001-CONTEXT.md",
199
+ researchPath: ".gsd/milestones/M001/M001-RESEARCH.md",
200
+ researchOutputPath: "/tmp/test-project/.gsd/milestones/M001/M001-RESEARCH.md",
201
+ outputPath: "/tmp/test-project/.gsd/milestones/M001/M001-ROADMAP.md",
202
+ secretsOutputPath: "/tmp/test-project/.gsd/milestones/M001/M001-SECRETS.md",
203
+ inlinedContext: "Context",
204
+ sourceFilePaths: "- source",
205
+ skillDiscoveryMode: "manual",
206
+ skillDiscoveryInstructions: " Discover skills manually.",
207
+ skillActivation: "Load milestone planning skills first.",
208
+ });
209
+
210
+ assert.ok(result.includes("Load milestone planning skills first."));
211
+ assert.ok(!result.includes("{{skillActivation}}"));
212
+ });
213
+
214
+ test("guided plan milestone prompt substitutes skillActivation", () => {
215
+ const result = loadPrompt("guided-plan-milestone", {
216
+ milestoneId: "M001",
217
+ milestoneTitle: "Test Milestone",
218
+ secretsOutputPath: ".gsd/milestones/M001/M001-SECRETS.md",
219
+ inlinedTemplates: "Templates",
220
+ skillActivation: "Load guided planning skills first.",
221
+ });
222
+
223
+ assert.ok(result.includes("Load guided planning skills first."));
224
+ assert.ok(!result.includes("{{skillActivation}}"));
225
+ });
226
+
227
+ test("guided plan slice prompt substitutes skillActivation", () => {
228
+ const result = loadPrompt("guided-plan-slice", {
229
+ milestoneId: "M001",
230
+ sliceId: "S01",
231
+ sliceTitle: "Test Slice",
232
+ inlinedTemplates: "Templates",
233
+ skillActivation: "Load guided slice planning skills first.",
234
+ });
235
+
236
+ assert.ok(result.includes("Load guided slice planning skills first."));
237
+ assert.ok(!result.includes("{{skillActivation}}"));
238
+ });
239
+
240
+ test("guided research slice prompt substitutes skillActivation", () => {
241
+ const result = loadPrompt("guided-research-slice", {
242
+ milestoneId: "M001",
243
+ sliceId: "S01",
244
+ sliceTitle: "Test Slice",
245
+ inlinedTemplates: "Templates",
246
+ skillActivation: "Load guided research skills first.",
247
+ });
248
+
249
+ assert.ok(result.includes("Load guided research skills first."));
250
+ assert.ok(!result.includes("{{skillActivation}}"));
251
+ });
@@ -208,30 +208,25 @@ test("git fields comprehensive validation", () => {
208
208
  assert.equal(preferences.git?.isolation, "branch");
209
209
  });
210
210
 
211
- test("auto_visualize, auto_report, compression_strategy, context_selection validate correctly", () => {
211
+ test("auto_visualize, auto_report, context_selection validate correctly", () => {
212
212
  const { preferences, errors } = validatePreferences({
213
213
  auto_visualize: true,
214
214
  auto_report: false,
215
- compression_strategy: "compress",
216
215
  context_selection: "smart",
217
216
  });
218
217
  assert.equal(errors.length, 0);
219
218
  assert.equal(preferences.auto_visualize, true);
220
219
  assert.equal(preferences.auto_report, false);
221
- assert.equal(preferences.compression_strategy, "compress");
222
220
  assert.equal(preferences.context_selection, "smart");
223
221
  });
224
222
 
225
- test("auto_visualize, auto_report, compression_strategy, context_selection reject invalid values", () => {
223
+ test("auto_visualize, auto_report, context_selection reject invalid values", () => {
226
224
  const { errors: e1 } = validatePreferences({ auto_visualize: "yes" as never });
227
225
  assert.ok(e1.some(e => e.includes("auto_visualize")));
228
226
 
229
227
  const { errors: e2 } = validatePreferences({ auto_report: 1 as never });
230
228
  assert.ok(e2.some(e => e.includes("auto_report")));
231
229
 
232
- const { errors: e3 } = validatePreferences({ compression_strategy: "shrink" as never });
233
- assert.ok(e3.some(e => e.includes("compression_strategy")));
234
-
235
230
  const { errors: e4 } = validatePreferences({ context_selection: "partial" as never });
236
231
  assert.ok(e4.some(e => e.includes("context_selection")));
237
232
  });
@@ -0,0 +1,59 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { readFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+
6
+ const promptsDir = join(process.cwd(), "src/resources/extensions/gsd/prompts");
7
+
8
+ function readPrompt(name: string): string {
9
+ return readFileSync(join(promptsDir, `${name}.md`), "utf-8");
10
+ }
11
+
12
+ test("reactive-execute prompt keeps task summaries with subagents and avoids batch commits", () => {
13
+ const prompt = readPrompt("reactive-execute");
14
+ assert.match(prompt, /subagent-written summary as authoritative/i);
15
+ assert.match(prompt, /Do NOT create a batch commit/i);
16
+ assert.doesNotMatch(prompt, /\*\*Write task summaries\*\*/i);
17
+ assert.doesNotMatch(prompt, /\*\*Commit\*\* all changes/i);
18
+ });
19
+
20
+ test("run-uat prompt branches on dynamic UAT mode and supports runtime evidence", () => {
21
+ const prompt = readPrompt("run-uat");
22
+ assert.match(prompt, /\*\*Detected UAT mode:\*\*\s*`\{\{uatType\}\}`/);
23
+ assert.match(prompt, /uatType:\s*\{\{uatType\}\}/);
24
+ assert.match(prompt, /live-runtime/);
25
+ assert.match(prompt, /browser\/runtime\/network/i);
26
+ assert.match(prompt, /NEEDS-HUMAN/);
27
+ assert.doesNotMatch(prompt, /uatType:\s*artifact-driven/);
28
+ });
29
+
30
+ test("workflow-start prompt defaults to autonomy instead of per-phase confirmation", () => {
31
+ const prompt = readPrompt("workflow-start");
32
+ assert.match(prompt, /Keep moving by default/i);
33
+ assert.match(prompt, /Decision gates, not ceremony/i);
34
+ assert.doesNotMatch(prompt, /confirm with the user before proceeding/i);
35
+ assert.doesNotMatch(prompt, /Gate between phases/i);
36
+ });
37
+
38
+ test("discuss prompt allows implementation questions when they materially matter", () => {
39
+ const prompt = readPrompt("discuss");
40
+ assert.match(prompt, /Lead with experience, but ask implementation when it materially matters/i);
41
+ assert.match(prompt, /one gate, not two/i);
42
+ assert.doesNotMatch(prompt, /Questions must be about the experience, not the implementation/i);
43
+ });
44
+
45
+ test("guided discussion prompts avoid wrap-up prompts after every round", () => {
46
+ const milestonePrompt = readPrompt("guided-discuss-milestone");
47
+ const slicePrompt = readPrompt("guided-discuss-slice");
48
+ assert.match(milestonePrompt, /Do \*\*not\*\* ask a meta "ready to wrap up\?" question after every round/i);
49
+ assert.match(slicePrompt, /Do \*\*not\*\* ask a meta "ready to wrap up\?" question after every round/i);
50
+ assert.doesNotMatch(milestonePrompt, /I think I have a solid picture of this milestone\. Ready to wrap up/i);
51
+ assert.doesNotMatch(slicePrompt, /I think I have a solid picture of this slice\. Ready to wrap up/i);
52
+ });
53
+
54
+ test("guided-resume-task prompt preserves recovery state until work is superseded", () => {
55
+ const prompt = readPrompt("guided-resume-task");
56
+ assert.match(prompt, /Do \*\*not\*\* delete the continue file immediately/i);
57
+ assert.match(prompt, /successfully completed or you have written a newer summary\/continue artifact/i);
58
+ assert.doesNotMatch(prompt, /Delete the continue file after reading it/i);
59
+ });
@@ -3,7 +3,7 @@ import { join } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { execSync } from "node:child_process";
5
5
 
6
- import { externalGsdRoot, ensureGsdSymlink } from "../repo-identity.ts";
6
+ import { repoIdentity, externalGsdRoot, ensureGsdSymlink, validateProjectId } from "../repo-identity.ts";
7
7
  import { createTestContext } from "./test-helpers.ts";
8
8
 
9
9
  const { assertEq, assertTrue, report } = createTestContext();
@@ -57,6 +57,26 @@ async function main(): Promise<void> {
57
57
  assertEq(preservedDirState, join(worktreePath, ".gsd"), "worktree .gsd directory is left in place for sync-based refresh");
58
58
  assertTrue(lstatSync(join(worktreePath, ".gsd")).isDirectory(), "worktree .gsd directory remains a directory");
59
59
  assertTrue(existsSync(join(worktreePath, ".gsd", "milestones", "stale.txt")), "existing worktree .gsd directory contents remain available for sync logic");
60
+
61
+ console.log("\n=== GSD_PROJECT_ID overrides computed repo hash ===");
62
+ process.env.GSD_PROJECT_ID = "my-project";
63
+ assertEq(repoIdentity(base), "my-project", "repoIdentity returns GSD_PROJECT_ID when set");
64
+ assertEq(externalGsdRoot(base), join(stateDir, "projects", "my-project"), "externalGsdRoot uses GSD_PROJECT_ID");
65
+ delete process.env.GSD_PROJECT_ID;
66
+
67
+ console.log("\n=== GSD_PROJECT_ID falls back to hash when unset ===");
68
+ const hashIdentity = repoIdentity(base);
69
+ assertTrue(/^[0-9a-f]{12}$/.test(hashIdentity), "repoIdentity returns 12-char hex hash when GSD_PROJECT_ID is unset");
70
+
71
+ console.log("\n=== validateProjectId rejects invalid values ===");
72
+ for (const invalid of ["has spaces", "path/traversal", "dot..dot", "back\\slash"]) {
73
+ assertTrue(!validateProjectId(invalid), `validateProjectId rejects invalid value: "${invalid}"`);
74
+ }
75
+
76
+ console.log("\n=== validateProjectId accepts valid values ===");
77
+ for (const valid of ["my-project", "foo_bar", "abc123", "A-Z_0-9"]) {
78
+ assertTrue(validateProjectId(valid), `validateProjectId accepts valid value: "${valid}"`);
79
+ }
60
80
  } finally {
61
81
  delete process.env.GSD_STATE_DIR;
62
82
  rmSync(base, { recursive: true, force: true });