gsd-pi 2.8.0 → 2.8.2

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/dist/loader.js +5 -0
  2. package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts +2 -0
  3. package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts.map +1 -1
  4. package/node_modules/@gsd/pi-coding-agent/dist/config.js +4 -0
  5. package/node_modules/@gsd/pi-coding-agent/dist/config.js.map +1 -1
  6. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
  7. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
  8. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js +117 -0
  9. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js +2 -2
  11. package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  12. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js +97 -0
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js.map +1 -0
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js +112 -3
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +32 -22
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
  30. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
  31. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
  32. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +3 -1
  33. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
  34. package/node_modules/@gsd/pi-coding-agent/dist/index.js +4 -1
  35. package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
  36. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  37. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
  38. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  39. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts +7 -0
  40. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  41. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js +11 -0
  42. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js.map +1 -1
  43. package/node_modules/@gsd/pi-coding-agent/src/config.ts +5 -0
  44. package/node_modules/@gsd/pi-coding-agent/src/core/artifact-manager.ts +125 -0
  45. package/node_modules/@gsd/pi-coding-agent/src/core/bash-executor.ts +2 -2
  46. package/node_modules/@gsd/pi-coding-agent/src/core/blob-store.ts +106 -0
  47. package/node_modules/@gsd/pi-coding-agent/src/core/session-manager.ts +119 -3
  48. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +35 -22
  49. package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
  50. package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
  51. package/node_modules/@gsd/pi-coding-agent/src/index.ts +4 -1
  52. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
  53. package/node_modules/@gsd/pi-coding-agent/src/utils/shell.ts +11 -0
  54. package/package.json +6 -1
  55. package/packages/pi-coding-agent/dist/config.d.ts +2 -0
  56. package/packages/pi-coding-agent/dist/config.d.ts.map +1 -1
  57. package/packages/pi-coding-agent/dist/config.js +4 -0
  58. package/packages/pi-coding-agent/dist/config.js.map +1 -1
  59. package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
  60. package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
  61. package/packages/pi-coding-agent/dist/core/artifact-manager.js +117 -0
  62. package/packages/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
  63. package/packages/pi-coding-agent/dist/core/bash-executor.js +2 -2
  64. package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  65. package/packages/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
  66. package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
  67. package/packages/pi-coding-agent/dist/core/blob-store.js +97 -0
  68. package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -0
  69. package/packages/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
  70. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/core/session-manager.js +112 -3
  72. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  74. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/tools/bash.js +32 -22
  76. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
  78. package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
  80. package/packages/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
  82. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
  84. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/index.d.ts +3 -1
  86. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/index.js +4 -1
  88. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
  91. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/utils/shell.d.ts +7 -0
  93. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/utils/shell.js +11 -0
  95. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  96. package/packages/pi-coding-agent/src/config.ts +5 -0
  97. package/packages/pi-coding-agent/src/core/artifact-manager.ts +125 -0
  98. package/packages/pi-coding-agent/src/core/bash-executor.ts +2 -2
  99. package/packages/pi-coding-agent/src/core/blob-store.ts +106 -0
  100. package/packages/pi-coding-agent/src/core/session-manager.ts +119 -3
  101. package/packages/pi-coding-agent/src/core/tools/bash.ts +35 -22
  102. package/packages/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
  103. package/packages/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
  104. package/packages/pi-coding-agent/src/index.ts +4 -1
  105. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
  106. package/packages/pi-coding-agent/src/utils/shell.ts +11 -0
  107. package/src/resources/extensions/bg-shell/index.ts +2 -1
  108. package/src/resources/extensions/browser-tools/lifecycle.ts +6 -1
  109. package/src/resources/extensions/gsd/auto.ts +92 -49
  110. package/src/resources/extensions/gsd/dispatch-guard.ts +65 -0
  111. package/src/resources/extensions/gsd/docs/preferences-reference.md +76 -0
  112. package/src/resources/extensions/gsd/exit-command.ts +18 -0
  113. package/src/resources/extensions/gsd/files.ts +9 -40
  114. package/src/resources/extensions/gsd/git-service.ts +62 -17
  115. package/src/resources/extensions/gsd/gitignore.ts +28 -0
  116. package/src/resources/extensions/gsd/guided-flow.ts +49 -11
  117. package/src/resources/extensions/gsd/index.ts +111 -16
  118. package/src/resources/extensions/gsd/preferences.ts +8 -0
  119. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
  120. package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -3
  121. package/src/resources/extensions/gsd/prompts/discuss.md +27 -2
  122. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  123. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -3
  124. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  125. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -2
  126. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -3
  127. package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  128. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  129. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  130. package/src/resources/extensions/gsd/prompts/run-uat.md +4 -4
  131. package/src/resources/extensions/gsd/roadmap-slices.ts +50 -0
  132. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +102 -0
  133. package/src/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
  134. package/src/resources/extensions/gsd/tests/git-service.test.ts +116 -39
  135. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +5 -5
  136. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +2 -1
  137. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +59 -0
  138. package/src/resources/extensions/gsd/tests/run-uat.test.ts +2 -4
  139. package/src/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
  140. package/src/resources/extensions/ttsr/index.ts +163 -0
  141. package/src/resources/extensions/ttsr/rule-loader.ts +121 -0
  142. package/src/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
  143. package/src/resources/extensions/ttsr/ttsr-manager.ts +344 -0
@@ -48,14 +48,14 @@ import {
48
48
  validateCompleteBoundary,
49
49
  formatValidationIssues,
50
50
  } from "./observability-validator.js";
51
- import { ensureGitignore } from "./gitignore.js";
51
+ import { ensureGitignore, untrackRuntimeFiles } from "./gitignore.js";
52
52
  import { runGSDDoctor, rebuildState } from "./doctor.js";
53
53
  import { snapshotSkills, clearSkillSnapshot } from "./skill-discovery.js";
54
54
  import {
55
55
  initMetrics, resetMetrics, snapshotUnitMetrics, getLedger,
56
56
  getProjectTotals, formatCost, formatTokenCount,
57
57
  } from "./metrics.js";
58
- import { join } from "node:path";
58
+ import { dirname, join } from "node:path";
59
59
  import { readdirSync, readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
60
60
  import { execSync, execFileSync } from "node:child_process";
61
61
  import {
@@ -68,6 +68,7 @@ import {
68
68
  mergeSliceToMain,
69
69
  } from "./worktree.ts";
70
70
  import { GitServiceImpl } from "./git-service.ts";
71
+ import { getPriorSliceCompletionBlocker } from "./dispatch-guard.ts";
71
72
  import type { GitPreferences } from "./git-service.ts";
72
73
  import { truncateToWidth, visibleWidth } from "@gsd/pi-tui";
73
74
  import { makeUI, GLYPH, INDENT } from "../shared/ui.js";
@@ -380,6 +381,7 @@ export async function startAuto(
380
381
 
381
382
  // Ensure .gitignore has baseline patterns
382
383
  ensureGitignore(base);
384
+ untrackRuntimeFiles(base);
383
385
 
384
386
  // Bootstrap .gsd/ if it doesn't exist
385
387
  const gsdDir = join(base, ".gsd");
@@ -1254,7 +1256,14 @@ async function dispatchNextUnit(
1254
1256
  }
1255
1257
  }
1256
1258
 
1257
- await emitObservabilityWarnings(ctx, unitType, unitId);
1259
+ const priorSliceBlocker = getPriorSliceCompletionBlocker(basePath, getMainBranch(basePath), unitType, unitId);
1260
+ if (priorSliceBlocker) {
1261
+ await stopAuto(ctx, pi);
1262
+ ctx.ui.notify(priorSliceBlocker, "error");
1263
+ return;
1264
+ }
1265
+
1266
+ const observabilityIssues = await collectObservabilityWarnings(ctx, unitType, unitId);
1258
1267
 
1259
1268
  // Idempotency: skip units already completed in a prior session.
1260
1269
  const idempotencyKey = `${unitType}/${unitId}`;
@@ -1394,6 +1403,13 @@ async function dispatchNextUnit(
1394
1403
  }
1395
1404
  }
1396
1405
 
1406
+ // Inject observability repair instructions so the agent fixes gaps before
1407
+ // proceeding with the unit (see #174).
1408
+ const repairBlock = buildObservabilityRepairBlock(observabilityIssues);
1409
+ if (repairBlock) {
1410
+ finalPrompt = `${finalPrompt}${repairBlock}`;
1411
+ }
1412
+
1397
1413
  // Switch model if preferences specify one for this unit type
1398
1414
  // Try primary model, then fallbacks in order if setting fails
1399
1415
  const modelConfig = resolveModelWithFallbacksForUnit(unitType);
@@ -1681,13 +1697,11 @@ async function buildResearchMilestonePrompt(mid: string, midTitle: string, base:
1681
1697
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1682
1698
 
1683
1699
  const outputRelPath = relMilestoneFile(base, mid, "RESEARCH");
1684
- const outputAbsPath = resolveMilestoneFile(base, mid, "RESEARCH") ?? join(base, outputRelPath);
1685
1700
  return loadPrompt("research-milestone", {
1686
1701
  milestoneId: mid, milestoneTitle: midTitle,
1687
1702
  milestonePath: relMilestonePath(base, mid),
1688
1703
  contextPath: contextRel,
1689
1704
  outputPath: outputRelPath,
1690
- outputAbsPath,
1691
1705
  inlinedContext,
1692
1706
  ...buildSkillDiscoveryVars(),
1693
1707
  });
@@ -1715,7 +1729,6 @@ async function buildPlanMilestonePrompt(mid: string, midTitle: string, base: str
1715
1729
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1716
1730
 
1717
1731
  const outputRelPath = relMilestoneFile(base, mid, "ROADMAP");
1718
- const outputAbsPath = resolveMilestoneFile(base, mid, "ROADMAP") ?? join(base, outputRelPath);
1719
1732
  const secretsOutputPath = relMilestoneFile(base, mid, "SECRETS");
1720
1733
  return loadPrompt("plan-milestone", {
1721
1734
  milestoneId: mid, milestoneTitle: midTitle,
@@ -1723,7 +1736,6 @@ async function buildPlanMilestonePrompt(mid: string, midTitle: string, base: str
1723
1736
  contextPath: contextRel,
1724
1737
  researchPath: researchRel,
1725
1738
  outputPath: outputRelPath,
1726
- outputAbsPath,
1727
1739
  secretsOutputPath,
1728
1740
  inlinedContext,
1729
1741
  });
@@ -1755,7 +1767,6 @@ async function buildResearchSlicePrompt(
1755
1767
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1756
1768
 
1757
1769
  const outputRelPath = relSliceFile(base, mid, sid, "RESEARCH");
1758
- const outputAbsPath = resolveSliceFile(base, mid, sid, "RESEARCH") ?? join(base, outputRelPath);
1759
1770
  return loadPrompt("research-slice", {
1760
1771
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
1761
1772
  slicePath: relSlicePath(base, mid, sid),
@@ -1763,7 +1774,6 @@ async function buildResearchSlicePrompt(
1763
1774
  contextPath: contextRel,
1764
1775
  milestoneResearchPath: milestoneResearchRel,
1765
1776
  outputPath: outputRelPath,
1766
- outputAbsPath,
1767
1777
  inlinedContext,
1768
1778
  dependencySummaries: depContent,
1769
1779
  ...buildSkillDiscoveryVars(),
@@ -1792,16 +1802,12 @@ async function buildPlanSlicePrompt(
1792
1802
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1793
1803
 
1794
1804
  const outputRelPath = relSliceFile(base, mid, sid, "PLAN");
1795
- const outputAbsPath = resolveSliceFile(base, mid, sid, "PLAN") ?? join(base, outputRelPath);
1796
- const sliceAbsPath = resolveSlicePath(base, mid, sid) ?? join(base, relSlicePath(base, mid, sid));
1797
1805
  return loadPrompt("plan-slice", {
1798
1806
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
1799
1807
  slicePath: relSlicePath(base, mid, sid),
1800
- sliceAbsPath,
1801
1808
  roadmapPath: roadmapRel,
1802
1809
  researchPath: researchRel,
1803
1810
  outputPath: outputRelPath,
1804
- outputAbsPath,
1805
1811
  inlinedContext,
1806
1812
  dependencySummaries: depContent,
1807
1813
  });
@@ -1852,8 +1858,7 @@ async function buildExecuteTaskPrompt(
1852
1858
 
1853
1859
  const carryForwardSection = await buildCarryForwardSection(priorSummaries, base);
1854
1860
 
1855
- const sliceDirAbs = resolveSlicePath(base, mid, sid) ?? join(base, relSlicePath(base, mid, sid));
1856
- const taskSummaryAbsPath = join(sliceDirAbs, "tasks", `${tid}-SUMMARY.md`);
1861
+ const taskSummaryPath = `${relSlicePath(base, mid, sid)}/tasks/${tid}-SUMMARY.md`;
1857
1862
 
1858
1863
  return loadPrompt("execute-task", {
1859
1864
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle, taskId: tid, taskTitle: tTitle,
@@ -1865,7 +1870,7 @@ async function buildExecuteTaskPrompt(
1865
1870
  carryForwardSection,
1866
1871
  resumeSection,
1867
1872
  priorTaskLines: priorLines,
1868
- taskSummaryAbsPath,
1873
+ taskSummaryPath,
1869
1874
  });
1870
1875
  }
1871
1876
 
@@ -1901,17 +1906,17 @@ async function buildCompleteSlicePrompt(
1901
1906
 
1902
1907
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1903
1908
 
1904
- const sliceDirAbs = resolveSlicePath(base, mid, sid) ?? join(base, relSlicePath(base, mid, sid));
1905
- const sliceSummaryAbsPath = join(sliceDirAbs, `${sid}-SUMMARY.md`);
1906
- const sliceUatAbsPath = join(sliceDirAbs, `${sid}-UAT.md`);
1909
+ const sliceRel = relSlicePath(base, mid, sid);
1910
+ const sliceSummaryPath = `${sliceRel}/${sid}-SUMMARY.md`;
1911
+ const sliceUatPath = `${sliceRel}/${sid}-UAT.md`;
1907
1912
 
1908
1913
  return loadPrompt("complete-slice", {
1909
1914
  milestoneId: mid, sliceId: sid, sliceTitle: sTitle,
1910
- slicePath: relSlicePath(base, mid, sid),
1915
+ slicePath: sliceRel,
1911
1916
  roadmapPath: roadmapRel,
1912
1917
  inlinedContext,
1913
- sliceSummaryAbsPath,
1914
- sliceUatAbsPath,
1918
+ sliceSummaryPath,
1919
+ sliceUatPath,
1915
1920
  });
1916
1921
  }
1917
1922
 
@@ -1950,15 +1955,14 @@ async function buildCompleteMilestonePrompt(
1950
1955
 
1951
1956
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
1952
1957
 
1953
- const milestoneDirAbs = resolveMilestonePath(base, mid) ?? join(base, relMilestonePath(base, mid));
1954
- const milestoneSummaryAbsPath = join(milestoneDirAbs, `${mid}-SUMMARY.md`);
1958
+ const milestoneSummaryPath = `${relMilestonePath(base, mid)}/${mid}-SUMMARY.md`;
1955
1959
 
1956
1960
  return loadPrompt("complete-milestone", {
1957
1961
  milestoneId: mid,
1958
1962
  milestoneTitle: midTitle,
1959
1963
  roadmapPath: roadmapRel,
1960
1964
  inlinedContext,
1961
- milestoneSummaryAbsPath,
1965
+ milestoneSummaryPath,
1962
1966
  });
1963
1967
  }
1964
1968
 
@@ -2001,8 +2005,7 @@ async function buildReplanSlicePrompt(
2001
2005
 
2002
2006
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
2003
2007
 
2004
- const sliceDirAbs = resolveSlicePath(base, mid, sid) ?? join(base, relSlicePath(base, mid, sid));
2005
- const replanAbsPath = join(sliceDirAbs, `${sid}-REPLAN.md`);
2008
+ const replanPath = `${relSlicePath(base, mid, sid)}/${sid}-REPLAN.md`;
2006
2009
 
2007
2010
  return loadPrompt("replan-slice", {
2008
2011
  milestoneId: mid,
@@ -2012,7 +2015,7 @@ async function buildReplanSlicePrompt(
2012
2015
  planPath: slicePlanRel,
2013
2016
  blockerTaskId,
2014
2017
  inlinedContext,
2015
- replanAbsPath,
2018
+ replanPath,
2016
2019
  });
2017
2020
  }
2018
2021
 
@@ -2130,8 +2133,6 @@ async function buildRunUatPrompt(
2130
2133
 
2131
2134
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
2132
2135
 
2133
- const sliceDirAbs = resolveSlicePath(base, mid, sliceId) ?? join(base, relSlicePath(base, mid, sliceId));
2134
- const uatResultAbsPath = join(sliceDirAbs, `${sliceId}-UAT-RESULT.md`);
2135
2136
  const uatResultPath = relSliceFile(base, mid, sliceId, "UAT-RESULT");
2136
2137
  const uatType = extractUatType(uatContent) ?? "human-experience";
2137
2138
 
@@ -2139,7 +2140,6 @@ async function buildRunUatPrompt(
2139
2140
  milestoneId: mid,
2140
2141
  sliceId,
2141
2142
  uatPath,
2142
- uatResultAbsPath,
2143
2143
  uatResultPath,
2144
2144
  uatType,
2145
2145
  inlinedContext,
@@ -2166,9 +2166,7 @@ async function buildReassessRoadmapPrompt(
2166
2166
 
2167
2167
  const inlinedContext = `## Inlined Context (preloaded — do not re-read these files)\n\n${inlined.join("\n\n---\n\n")}`;
2168
2168
 
2169
- const assessmentRel = relSliceFile(base, mid, completedSliceId, "ASSESSMENT");
2170
- const sliceDirAbs = resolveSlicePath(base, mid, completedSliceId) ?? join(base, relSlicePath(base, mid, completedSliceId));
2171
- const assessmentAbsPath = join(sliceDirAbs, `${completedSliceId}-ASSESSMENT.md`);
2169
+ const assessmentPath = relSliceFile(base, mid, completedSliceId, "ASSESSMENT");
2172
2170
 
2173
2171
  return loadPrompt("reassess-roadmap", {
2174
2172
  milestoneId: mid,
@@ -2176,8 +2174,7 @@ async function buildReassessRoadmapPrompt(
2176
2174
  completedSliceId,
2177
2175
  roadmapPath: roadmapRel,
2178
2176
  completedSliceSummaryPath: summaryRel,
2179
- assessmentPath: assessmentRel,
2180
- assessmentAbsPath,
2177
+ assessmentPath,
2181
2178
  inlinedContext,
2182
2179
  });
2183
2180
  }
@@ -2357,17 +2354,17 @@ function ensurePreconditions(
2357
2354
 
2358
2355
  // ─── Diagnostics ──────────────────────────────────────────────────────────────
2359
2356
 
2360
- async function emitObservabilityWarnings(
2357
+ async function collectObservabilityWarnings(
2361
2358
  ctx: ExtensionContext,
2362
2359
  unitType: string,
2363
2360
  unitId: string,
2364
- ): Promise<void> {
2361
+ ): Promise<import("./observability-validator.ts").ValidationIssue[]> {
2365
2362
  const parts = unitId.split("/");
2366
2363
  const mid = parts[0];
2367
2364
  const sid = parts[1];
2368
2365
  const tid = parts[2];
2369
2366
 
2370
- if (!mid || !sid) return;
2367
+ if (!mid || !sid) return [];
2371
2368
 
2372
2369
  let issues = [] as Awaited<ReturnType<typeof validatePlanBoundary>>;
2373
2370
 
@@ -2379,12 +2376,38 @@ async function emitObservabilityWarnings(
2379
2376
  issues = await validateCompleteBoundary(basePath, mid, sid);
2380
2377
  }
2381
2378
 
2382
- if (issues.length === 0) return;
2379
+ if (issues.length > 0) {
2380
+ ctx.ui.notify(
2381
+ `Observability check (${unitType}) found ${issues.length} warning${issues.length === 1 ? "" : "s"}:\n${formatValidationIssues(issues)}`,
2382
+ "warning",
2383
+ );
2384
+ }
2383
2385
 
2384
- ctx.ui.notify(
2385
- `Observability check (${unitType}) found ${issues.length} warning${issues.length === 1 ? "" : "s"}:\n${formatValidationIssues(issues)}`,
2386
- "warning",
2387
- );
2386
+ return issues;
2387
+ }
2388
+
2389
+ function buildObservabilityRepairBlock(issues: import("./observability-validator.ts").ValidationIssue[]): string {
2390
+ if (issues.length === 0) return "";
2391
+ const items = issues.map(issue => {
2392
+ const fileName = issue.file.split("/").pop() || issue.file;
2393
+ let line = `- **${fileName}**: ${issue.message}`;
2394
+ if (issue.suggestion) line += ` → ${issue.suggestion}`;
2395
+ return line;
2396
+ });
2397
+ return [
2398
+ "",
2399
+ "---",
2400
+ "",
2401
+ "## Pre-flight: Observability gaps to fix FIRST",
2402
+ "",
2403
+ "The following issues were detected in plan/summary files for this unit.",
2404
+ "**Read each flagged file, apply the fix described, then proceed with the unit.**",
2405
+ "",
2406
+ ...items,
2407
+ "",
2408
+ "---",
2409
+ "",
2410
+ ].join("\n");
2388
2411
  }
2389
2412
 
2390
2413
  async function recoverTimedOutUnit(
@@ -2736,14 +2759,34 @@ export function resolveExpectedArtifactPath(unitType: string, unitId: string, ba
2736
2759
  }
2737
2760
 
2738
2761
  /**
2739
- * Check whether the expected artifact for a unit exists on disk.
2740
- * Returns true if the artifact file exists, or if the unit type has no
2762
+ * Check whether the expected artifact(s) for a unit exist on disk.
2763
+ * Returns true if all required artifacts exist, or if the unit type has no
2741
2764
  * single verifiable artifact (e.g., replan-slice).
2765
+ *
2766
+ * complete-slice requires both SUMMARY and UAT files — verifying only
2767
+ * the summary allowed the unit to be marked complete when the LLM
2768
+ * skipped writing the UAT file (see #176).
2742
2769
  */
2743
2770
  function verifyExpectedArtifact(unitType: string, unitId: string, base: string): boolean {
2744
2771
  const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
2745
2772
  if (!absPath) return true;
2746
- return existsSync(absPath);
2773
+ if (!existsSync(absPath)) return false;
2774
+
2775
+ // complete-slice must also produce a UAT file
2776
+ if (unitType === "complete-slice") {
2777
+ const parts = unitId.split("/");
2778
+ const mid = parts[0];
2779
+ const sid = parts[1];
2780
+ if (mid && sid) {
2781
+ const dir = resolveSlicePath(base, mid, sid);
2782
+ if (dir) {
2783
+ const uatPath = join(dir, buildSliceFileName(sid, "UAT"));
2784
+ if (!existsSync(uatPath)) return false;
2785
+ }
2786
+ }
2787
+ }
2788
+
2789
+ return true;
2747
2790
  }
2748
2791
 
2749
2792
  /**
@@ -2753,7 +2796,7 @@ function verifyExpectedArtifact(unitType: string, unitId: string, base: string):
2753
2796
  export function writeBlockerPlaceholder(unitType: string, unitId: string, base: string, reason: string): string | null {
2754
2797
  const absPath = resolveExpectedArtifactPath(unitType, unitId, base);
2755
2798
  if (!absPath) return null;
2756
- const dir = absPath.substring(0, absPath.lastIndexOf("/"));
2799
+ const dir = dirname(absPath);
2757
2800
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
2758
2801
  const content = [
2759
2802
  `# BLOCKER — auto-mode recovery failed`,
@@ -2787,7 +2830,7 @@ function diagnoseExpectedArtifact(unitType: string, unitId: string, base: string
2787
2830
  return `Task ${tid} marked [x] in ${relSliceFile(base, mid!, sid!, "PLAN")} + summary written`;
2788
2831
  }
2789
2832
  case "complete-slice":
2790
- return `Slice ${sid} marked [x] in ${relMilestoneFile(base, mid!, "ROADMAP")} + summary written`;
2833
+ return `Slice ${sid} marked [x] in ${relMilestoneFile(base, mid!, "ROADMAP")} + summary + UAT written`;
2791
2834
  case "replan-slice":
2792
2835
  return `${relSliceFile(base, mid!, sid!, "REPLAN")} + updated ${relSliceFile(base, mid!, sid!, "PLAN")}`;
2793
2836
  case "reassess-roadmap":
@@ -0,0 +1,65 @@
1
+ import { execSync } from "node:child_process";
2
+ import { relMilestoneFile } from "./paths.js";
3
+ import { parseRoadmapSlices } from "./roadmap-slices.ts";
4
+
5
+ const SLICE_DISPATCH_TYPES = new Set([
6
+ "research-slice",
7
+ "plan-slice",
8
+ "replan-slice",
9
+ "execute-task",
10
+ "complete-slice",
11
+ ]);
12
+
13
+ function readTrackedFileFromBranch(base: string, branch: string, relPath: string): string | null {
14
+ try {
15
+ return execSync(`git show ${branch}:${relPath}`, {
16
+ cwd: base,
17
+ stdio: ["ignore", "pipe", "pipe"],
18
+ encoding: "utf-8",
19
+ }).trim();
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+
25
+ function milestoneIdFromNumber(num: number): string {
26
+ return `M${String(num).padStart(3, "0")}`;
27
+ }
28
+
29
+ export function getPriorSliceCompletionBlocker(base: string, mainBranch: string, unitType: string, unitId: string): string | null {
30
+ if (!SLICE_DISPATCH_TYPES.has(unitType)) return null;
31
+
32
+ const [targetMid, targetSid] = unitId.split("/");
33
+ if (!targetMid || !targetSid) return null;
34
+
35
+ const targetMidNumber = Number.parseInt(targetMid.slice(1), 10);
36
+ if (!Number.isFinite(targetMidNumber)) return null;
37
+
38
+ for (let milestoneNumber = 1; milestoneNumber <= targetMidNumber; milestoneNumber += 1) {
39
+ const mid = milestoneIdFromNumber(milestoneNumber);
40
+ const roadmapRel = relMilestoneFile(base, mid, "ROADMAP");
41
+ if (!roadmapRel) continue;
42
+
43
+ const roadmapContent = readTrackedFileFromBranch(base, mainBranch, roadmapRel);
44
+ if (!roadmapContent) continue;
45
+
46
+ const slices = parseRoadmapSlices(roadmapContent);
47
+ if (mid !== targetMid) {
48
+ const incomplete = slices.find(slice => !slice.done);
49
+ if (incomplete) {
50
+ return `Cannot dispatch ${unitType} ${unitId}: earlier slice ${mid}/${incomplete.id} is not complete on ${mainBranch}.`;
51
+ }
52
+ continue;
53
+ }
54
+
55
+ const targetIndex = slices.findIndex(slice => slice.id === targetSid);
56
+ if (targetIndex === -1) return null;
57
+
58
+ const incomplete = slices.slice(0, targetIndex).find(slice => !slice.done);
59
+ if (incomplete) {
60
+ return `Cannot dispatch ${unitType} ${unitId}: earlier slice ${targetMid}/${incomplete.id} is not complete on ${mainBranch}.`;
61
+ }
62
+ }
63
+
64
+ return null;
65
+ }
@@ -13,6 +13,61 @@ Full documentation for `~/.gsd/preferences.md` (global) and `.gsd/preferences.md
13
13
 
14
14
  ---
15
15
 
16
+ ## Semantics
17
+
18
+ ### Empty Arrays vs Omitted Fields
19
+
20
+ **Empty arrays (`[]`) are equivalent to omitting the field entirely.** During validation, GSD deletes empty arrays from the preferences object (see `validatePreferences()` in `preferences.ts`):
21
+
22
+ ```typescript
23
+ for (const key of ["always_use_skills", "prefer_skills", "avoid_skills", "custom_instructions"] as const) {
24
+ if (validated[key] && validated[key]!.length === 0) {
25
+ delete validated[key];
26
+ }
27
+ }
28
+ ```
29
+
30
+ These are functionally identical:
31
+
32
+ ```yaml
33
+ # Explicit empty arrays — will be normalized away
34
+ prefer_skills: []
35
+ avoid_skills: []
36
+ skill_rules: []
37
+
38
+ # Omitted entirely — same result
39
+ # (just don't write these fields)
40
+ ```
41
+
42
+ **Recommendation:** Omit fields you don't need. Empty arrays add noise with no effect.
43
+
44
+ ### Global vs Project Preferences
45
+
46
+ Preferences are loaded from two locations and merged:
47
+
48
+ 1. **Global:** `~/.gsd/preferences.md` — applies to all projects
49
+ 2. **Project:** `.gsd/preferences.md` — applies to the current project only
50
+
51
+ **Merge behavior** (see `mergePreferences()` in `preferences.ts`):
52
+ - **Scalar fields** (`skill_discovery`, `budget_ceiling`, etc.): Project wins if defined, otherwise global. Uses nullish coalescing (`??`).
53
+ - **Array fields** (`always_use_skills`, `prefer_skills`, etc.): Concatenated via `mergeStringLists()` (global first, then project).
54
+ - **Object fields** (`models`, `git`, `auto_supervisor`): Shallow merge via spread operator `{ ...base, ...override }`.
55
+
56
+ For `models`, project settings override global at the phase level. If global has `planning: opus` and project has `planning: sonnet`, the project wins. But if project omits `research`, global's `research` setting is preserved.
57
+
58
+ ### Skill Discovery vs Skill Preferences
59
+
60
+ These are **separate concerns**:
61
+
62
+ | Field | What it controls | Code reference |
63
+ |-------|-----------------|----------------|
64
+ | `skill_discovery` | **Whether** GSD looks for relevant skills during research | `resolveSkillDiscoveryMode()` in `preferences.ts` |
65
+ | `always_use_skills`, `prefer_skills`, `avoid_skills` | **Which** skills to use when they're found relevant | `renderPreferencesForSystemPrompt()` in `preferences.ts` |
66
+
67
+ Setting `prefer_skills: []` does **not** disable skill discovery — it just means you have no preference overrides. Use `skill_discovery: off` to disable discovery entirely.
68
+
69
+ ---
70
+
16
71
  ## Field Guide
17
72
 
18
73
  - `version`: schema version. Start at `1`.
@@ -60,6 +115,27 @@ Full documentation for `~/.gsd/preferences.md` (global) and `.gsd/preferences.md
60
115
  - Use `skill_rules` for situational routing, not broad personality preferences.
61
116
  - Prefer skill names for stable built-in skills.
62
117
  - Prefer absolute paths for local personal skills.
118
+ - **Omit fields you don't need** — empty arrays add noise with no effect.
119
+
120
+ ---
121
+
122
+ ## Minimal Example
123
+
124
+ The cleanest preferences file only specifies what you actually want:
125
+
126
+ ```yaml
127
+ ---
128
+ version: 1
129
+ always_use_skills:
130
+ - debug-like-expert
131
+ skill_discovery: suggest
132
+ models:
133
+ planning: claude-opus-4-6
134
+ execution: claude-sonnet-4-6
135
+ ---
136
+ ```
137
+
138
+ Everything else uses defaults. No `prefer_skills: []`, no `avoid_skills: []`, no `auto_supervisor: {}` — those are just noise.
63
139
 
64
140
  ---
65
141
 
@@ -0,0 +1,18 @@
1
+ import type { ExtensionAPI, ExtensionCommandContext } from "@gsd/pi-coding-agent";
2
+
3
+ type StopAutoFn = (ctx: ExtensionCommandContext, pi: ExtensionAPI) => Promise<void>;
4
+
5
+ export function registerExitCommand(
6
+ pi: ExtensionAPI,
7
+ deps: { stopAuto?: StopAutoFn } = {},
8
+ ): void {
9
+ pi.registerCommand("exit", {
10
+ description: "Exit GSD gracefully",
11
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
12
+ // Stop auto-mode first so locks and activity state are cleaned up before shutdown.
13
+ const stopAuto = deps.stopAuto ?? (await import("./auto.js")).stopAuto;
14
+ await stopAuto(ctx, pi);
15
+ ctx.shutdown();
16
+ },
17
+ });
18
+ }
@@ -1,14 +1,14 @@
1
- // GSD Extension File Parsing and I/O
1
+ // GSD Extension - File Parsing and I/O
2
2
  // Parsers for roadmap, plan, summary, and continue files.
3
3
  // Used by state derivation and the status widget.
4
- // Pure functions, zero Pi dependencies uses only Node built-ins.
4
+ // Pure functions, zero Pi dependencies - uses only Node built-ins.
5
5
 
6
6
  import { promises as fs, readdirSync } from 'node:fs';
7
7
  import { dirname, resolve } from 'node:path';
8
8
  import { milestonesDir, resolveMilestoneFile, relMilestoneFile } from './paths.js';
9
9
 
10
10
  import type {
11
- Roadmap, RoadmapSliceEntry, BoundaryMapEntry, RiskLevel,
11
+ Roadmap, BoundaryMapEntry,
12
12
  SlicePlan, TaskPlanEntry,
13
13
  Summary, SummaryFrontmatter, SummaryRequires, FileModified,
14
14
  Continue, ContinueFrontmatter, ContinueStatus,
@@ -18,6 +18,7 @@ import type {
18
18
  } from './types.ts';
19
19
 
20
20
  import { checkExistingEnvKeys } from '../get-secrets-from-user.ts';
21
+ import { parseRoadmapSlices } from './roadmap-slices.ts';
21
22
 
22
23
  // ─── Helpers ───────────────────────────────────────────────────────────────
23
24
 
@@ -199,40 +200,8 @@ export function parseRoadmap(content: string): Roadmap {
199
200
  })();
200
201
  const successCriteria = scSection ? parseBullets(scSection) : [];
201
202
 
202
- // Slices
203
- const slicesSection = extractSection(content, 'Slices');
204
- const slices: RoadmapSliceEntry[] = [];
205
-
206
- if (slicesSection) {
207
- const checkboxItems = slicesSection.split('\n');
208
- let currentSlice: RoadmapSliceEntry | null = null;
209
-
210
- for (const line of checkboxItems) {
211
- const cbMatch = line.match(/^-\s+\[([ xX])\]\s+\*\*(\w+):\s+(.+?)\*\*\s*(.*)/);
212
- if (cbMatch) {
213
- if (currentSlice) slices.push(currentSlice);
214
-
215
- const done = cbMatch[1].toLowerCase() === 'x';
216
- const id = cbMatch[2];
217
- const sliceTitle = cbMatch[3];
218
- const rest = cbMatch[4];
219
-
220
- const riskMatch = rest.match(/`risk:(\w+)`/);
221
- const risk = (riskMatch ? riskMatch[1] : 'low') as RiskLevel;
222
-
223
- const depsMatch = rest.match(/`depends:\[([^\]]*)\]`/);
224
- const depends = depsMatch && depsMatch[1].trim()
225
- ? depsMatch[1].split(',').map(s => s.trim())
226
- : [];
227
-
228
- currentSlice = { id, title: sliceTitle, risk, depends, done, demo: '' };
229
- } else if (currentSlice && line.trim().startsWith('>')) {
230
- const demoText = line.trim().replace(/^>\s*/, '').replace(/^After this:\s*/i, '');
231
- currentSlice.demo = demoText;
232
- }
233
- }
234
- if (currentSlice) slices.push(currentSlice);
235
- }
203
+ // Slices
204
+ const slices = parseRoadmapSlices(content);
236
205
 
237
206
  // Boundary map
238
207
  const boundaryMap: BoundaryMapEntry[] = [];
@@ -657,7 +626,7 @@ export function parseTaskPlanMustHaves(content: string): Array<{ text: string; c
657
626
  checked: cbMatch[1].toLowerCase() === 'x',
658
627
  };
659
628
  }
660
- // No checkbox treat as unchecked with full line as text
629
+ // No checkbox - treat as unchecked with full line as text
661
630
  return { text: line.trim(), checked: false };
662
631
  });
663
632
  }
@@ -732,7 +701,7 @@ export type UatType = 'artifact-driven' | 'live-runtime' | 'human-experience' |
732
701
  /**
733
702
  * Extract the UAT type from a UAT file's raw content.
734
703
  *
735
- * UAT files have no YAML frontmatter pass raw file content directly.
704
+ * UAT files have no YAML frontmatter - pass raw file content directly.
736
705
  * Classification is leading-keyword-only: e.g. `mixed (artifact-driven + live-runtime)` → `'mixed'`.
737
706
  *
738
707
  * Returns `undefined` when:
@@ -811,7 +780,7 @@ export async function inlinePriorMilestoneSummary(mid: string, base: string): Pr
811
780
  * with the current environment (.env + process.env).
812
781
  *
813
782
  * Returns `null` when no manifest file exists (path resolution failure or
814
- * file not on disk) callers can distinguish "no manifest" from "empty manifest".
783
+ * file not on disk) - callers can distinguish "no manifest" from "empty manifest".
815
784
  */
816
785
  export async function getManifestStatus(
817
786
  base: string, milestoneId: string,