gsd-pi 2.38.0-dev.8f5c161 → 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 (143) hide show
  1. package/README.md +15 -11
  2. package/dist/resource-loader.js +34 -1
  3. package/dist/resources/extensions/browser-tools/index.js +3 -1
  4. package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
  5. package/dist/resources/extensions/github-sync/cli.js +284 -0
  6. package/dist/resources/extensions/github-sync/index.js +73 -0
  7. package/dist/resources/extensions/github-sync/mapping.js +67 -0
  8. package/dist/resources/extensions/github-sync/sync.js +424 -0
  9. package/dist/resources/extensions/github-sync/templates.js +118 -0
  10. package/dist/resources/extensions/github-sync/types.js +7 -0
  11. package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
  12. package/dist/resources/extensions/gsd/auto-loop.js +538 -469
  13. package/dist/resources/extensions/gsd/auto-post-unit.js +28 -3
  14. package/dist/resources/extensions/gsd/auto-prompts.js +197 -19
  15. package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
  16. package/dist/resources/extensions/gsd/commands.js +2 -1
  17. package/dist/resources/extensions/gsd/doctor-providers.js +3 -0
  18. package/dist/resources/extensions/gsd/doctor.js +20 -1
  19. package/dist/resources/extensions/gsd/exit-command.js +2 -1
  20. package/dist/resources/extensions/gsd/files.js +46 -7
  21. package/dist/resources/extensions/gsd/git-service.js +30 -12
  22. package/dist/resources/extensions/gsd/gitignore.js +16 -3
  23. package/dist/resources/extensions/gsd/guided-flow.js +149 -38
  24. package/dist/resources/extensions/gsd/health-widget-core.js +32 -70
  25. package/dist/resources/extensions/gsd/health-widget.js +3 -86
  26. package/dist/resources/extensions/gsd/index.js +22 -19
  27. package/dist/resources/extensions/gsd/migrate-external.js +18 -1
  28. package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
  29. package/dist/resources/extensions/gsd/paths.js +3 -0
  30. package/dist/resources/extensions/gsd/preferences-types.js +1 -0
  31. package/dist/resources/extensions/gsd/preferences-validation.js +58 -0
  32. package/dist/resources/extensions/gsd/preferences.js +20 -9
  33. package/dist/resources/extensions/gsd/prompt-loader.js +6 -2
  34. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  36. package/dist/resources/extensions/gsd/prompts/execute-task.md +3 -1
  37. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  39. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  40. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  41. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  42. package/dist/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  43. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  44. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  45. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  46. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  47. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  48. package/dist/resources/extensions/gsd/prompts/run-uat.md +3 -1
  49. package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
  50. package/dist/resources/extensions/gsd/state.js +41 -22
  51. package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
  52. package/dist/resources/extensions/gsd/templates/task-plan.md +3 -0
  53. package/dist/resources/extensions/mcp-client/index.js +14 -1
  54. package/dist/resources/extensions/remote-questions/status.js +4 -2
  55. package/dist/resources/extensions/remote-questions/store.js +4 -2
  56. package/dist/resources/extensions/shared/frontmatter.js +1 -1
  57. package/package.json +1 -1
  58. package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
  59. package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
  60. package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
  61. package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
  62. package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
  63. package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
  64. package/packages/pi-coding-agent/dist/core/skills.d.ts +1 -0
  65. package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
  66. package/packages/pi-coding-agent/dist/core/skills.js +6 -1
  67. package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
  68. package/packages/pi-coding-agent/dist/index.d.ts +1 -1
  69. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  70. package/packages/pi-coding-agent/dist/index.js +1 -1
  71. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  72. package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
  73. package/packages/pi-coding-agent/src/core/skills.ts +9 -1
  74. package/packages/pi-coding-agent/src/index.ts +1 -0
  75. package/src/resources/extensions/browser-tools/index.ts +3 -0
  76. package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
  77. package/src/resources/extensions/github-sync/cli.ts +364 -0
  78. package/src/resources/extensions/github-sync/index.ts +93 -0
  79. package/src/resources/extensions/github-sync/mapping.ts +81 -0
  80. package/src/resources/extensions/github-sync/sync.ts +556 -0
  81. package/src/resources/extensions/github-sync/templates.ts +183 -0
  82. package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
  83. package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
  84. package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
  85. package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
  86. package/src/resources/extensions/github-sync/types.ts +47 -0
  87. package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
  88. package/src/resources/extensions/gsd/auto-loop.ts +342 -304
  89. package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
  90. package/src/resources/extensions/gsd/auto-prompts.ts +242 -19
  91. package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
  92. package/src/resources/extensions/gsd/commands.ts +2 -2
  93. package/src/resources/extensions/gsd/doctor-providers.ts +4 -0
  94. package/src/resources/extensions/gsd/doctor.ts +22 -1
  95. package/src/resources/extensions/gsd/exit-command.ts +2 -2
  96. package/src/resources/extensions/gsd/files.ts +49 -9
  97. package/src/resources/extensions/gsd/git-service.ts +44 -10
  98. package/src/resources/extensions/gsd/gitignore.ts +17 -3
  99. package/src/resources/extensions/gsd/guided-flow.ts +177 -44
  100. package/src/resources/extensions/gsd/health-widget-core.ts +28 -80
  101. package/src/resources/extensions/gsd/health-widget.ts +3 -89
  102. package/src/resources/extensions/gsd/index.ts +21 -16
  103. package/src/resources/extensions/gsd/migrate-external.ts +18 -1
  104. package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
  105. package/src/resources/extensions/gsd/paths.ts +4 -0
  106. package/src/resources/extensions/gsd/preferences-types.ts +4 -0
  107. package/src/resources/extensions/gsd/preferences-validation.ts +50 -0
  108. package/src/resources/extensions/gsd/preferences.ts +23 -9
  109. package/src/resources/extensions/gsd/prompt-loader.ts +7 -2
  110. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  111. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  112. package/src/resources/extensions/gsd/prompts/execute-task.md +3 -1
  113. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -1
  114. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -1
  115. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +1 -1
  116. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -1
  117. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +1 -1
  118. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -1
  119. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  120. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  121. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  122. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  123. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  124. package/src/resources/extensions/gsd/prompts/run-uat.md +3 -1
  125. package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
  126. package/src/resources/extensions/gsd/state.ts +38 -20
  127. package/src/resources/extensions/gsd/templates/runtime.md +21 -0
  128. package/src/resources/extensions/gsd/templates/task-plan.md +3 -0
  129. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +106 -31
  130. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +4 -3
  131. package/src/resources/extensions/gsd/tests/derive-state.test.ts +43 -0
  132. package/src/resources/extensions/gsd/tests/gitignore-tracked-gsd.test.ts +50 -0
  133. package/src/resources/extensions/gsd/tests/health-widget.test.ts +16 -54
  134. package/src/resources/extensions/gsd/tests/parsers.test.ts +131 -14
  135. package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +209 -0
  136. package/src/resources/extensions/gsd/tests/run-uat.test.ts +5 -1
  137. package/src/resources/extensions/gsd/tests/skill-activation.test.ts +140 -0
  138. package/src/resources/extensions/gsd/types.ts +18 -0
  139. package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
  140. package/src/resources/extensions/mcp-client/index.ts +17 -1
  141. package/src/resources/extensions/remote-questions/status.ts +4 -2
  142. package/src/resources/extensions/remote-questions/store.ts +4 -2
  143. package/src/resources/extensions/shared/frontmatter.ts +1 -1
@@ -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
+ });
@@ -29,7 +29,11 @@ const worktreePromptsDir = join(__dirname, '..', 'prompts');
29
29
  function loadPromptFromWorktree(name: string, vars: Record<string, string> = {}): string {
30
30
  const path = join(worktreePromptsDir, `${name}.md`);
31
31
  let content = readFileSync(path, 'utf-8');
32
- for (const [key, value] of Object.entries(vars)) {
32
+ const effectiveVars = {
33
+ skillActivation: 'If no installed skill clearly matches this unit, skip explicit skill activation and continue with the required workflow.',
34
+ ...vars,
35
+ };
36
+ for (const [key, value] of Object.entries(effectiveVars)) {
33
37
  content = content.replaceAll(`{{${key}}}`, value);
34
38
  }
35
39
  return content.trim();
@@ -0,0 +1,140 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+ import { loadSkills } from "@gsd/pi-coding-agent";
7
+ import { buildSkillActivationBlock } from "../auto-prompts.js";
8
+ import type { GSDPreferences } from "../preferences.js";
9
+
10
+ function makeTempBase(): string {
11
+ return mkdtempSync(join(tmpdir(), "gsd-skill-activation-"));
12
+ }
13
+
14
+ function cleanup(base: string): void {
15
+ rmSync(base, { recursive: true, force: true });
16
+ }
17
+
18
+ function writeSkill(base: string, name: string, description: string): void {
19
+ const dir = join(base, "skills", name);
20
+ mkdirSync(dir, { recursive: true });
21
+ writeFileSync(join(dir, "SKILL.md"), `---\nname: ${name}\ndescription: ${description}\n---\n\n# ${name}\n`);
22
+ }
23
+
24
+ function loadOnlyTestSkills(base: string): void {
25
+ loadSkills({ cwd: base, includeDefaults: false, skillPaths: [join(base, "skills")] });
26
+ }
27
+
28
+ function buildBlock(
29
+ base: string,
30
+ params: Partial<Parameters<typeof buildSkillActivationBlock>[0]> = {},
31
+ preferences: GSDPreferences = {},
32
+ ): string {
33
+ return buildSkillActivationBlock({
34
+ base,
35
+ milestoneId: "M001",
36
+ sliceId: "S01",
37
+ ...params,
38
+ preferences,
39
+ });
40
+ }
41
+
42
+ test("buildSkillActivationBlock matches installed skills from task context", () => {
43
+ const base = makeTempBase();
44
+ try {
45
+ writeSkill(base, "react", "Use for React components, hooks, JSX, and frontend UI work.");
46
+ writeSkill(base, "swiftui", "Use for SwiftUI views, iOS layout, and Apple platform UI work.");
47
+ loadOnlyTestSkills(base);
48
+
49
+ const result = buildBlock(base, {
50
+ sliceTitle: "Build React dashboard",
51
+ taskId: "T01",
52
+ taskTitle: "Implement React settings panel",
53
+ });
54
+
55
+ assert.match(result, /<skill_activation>/);
56
+ assert.match(result, /Call Skill\('react'\)/);
57
+ assert.doesNotMatch(result, /swiftui/);
58
+ } finally {
59
+ cleanup(base);
60
+ }
61
+ });
62
+
63
+ test("buildSkillActivationBlock includes always_use_skills from preferences", () => {
64
+ const base = makeTempBase();
65
+ try {
66
+ writeSkill(base, "testing", "Use for test setup, assertions, and verification patterns.");
67
+ loadOnlyTestSkills(base);
68
+
69
+ const result = buildBlock(base, { taskTitle: "Unrelated task title" }, {
70
+ always_use_skills: ["testing"],
71
+ });
72
+
73
+ assert.match(result, /Call Skill\('testing'\)/);
74
+ } finally {
75
+ cleanup(base);
76
+ }
77
+ });
78
+
79
+ test("buildSkillActivationBlock includes skill_rules matches and task-plan skills_used", () => {
80
+ const base = makeTempBase();
81
+ try {
82
+ writeSkill(base, "prisma", "Use for Prisma schema, migrations, and ORM queries.");
83
+ writeSkill(base, "accessibility", "Use for accessibility, aria attributes, and keyboard support.");
84
+ loadOnlyTestSkills(base);
85
+
86
+ const taskPlan = [
87
+ "---",
88
+ "skills_used:",
89
+ " - accessibility",
90
+ "---",
91
+ "# T01: Example",
92
+ ].join("\n");
93
+
94
+ const result = buildBlock(base, {
95
+ taskTitle: "Update prisma schema",
96
+ taskPlanContent: taskPlan,
97
+ }, {
98
+ skill_rules: [{ when: "prisma database schema", use: ["prisma"] }],
99
+ });
100
+
101
+ assert.match(result, /Call Skill\('accessibility'\)/);
102
+ assert.match(result, /Call Skill\('prisma'\)/);
103
+ } finally {
104
+ cleanup(base);
105
+ }
106
+ });
107
+
108
+ test("buildSkillActivationBlock honors avoid_skills", () => {
109
+ const base = makeTempBase();
110
+ try {
111
+ writeSkill(base, "react", "Use for React components and frontend UI work.");
112
+ loadOnlyTestSkills(base);
113
+
114
+ const result = buildBlock(base, {
115
+ taskTitle: "Implement React settings panel",
116
+ }, {
117
+ avoid_skills: ["react"],
118
+ });
119
+
120
+ assert.equal(result, "");
121
+ } finally {
122
+ cleanup(base);
123
+ }
124
+ });
125
+
126
+ test("buildSkillActivationBlock falls back cleanly when nothing matches", () => {
127
+ const base = makeTempBase();
128
+ try {
129
+ writeSkill(base, "swiftui", "Use for SwiftUI apps.");
130
+ loadOnlyTestSkills(base);
131
+
132
+ const result = buildBlock(base, {
133
+ taskTitle: "Plain text docs task",
134
+ });
135
+
136
+ assert.equal(result, "");
137
+ } finally {
138
+ cleanup(base);
139
+ }
140
+ });