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
@@ -5,10 +5,9 @@
5
5
  * runtime integrations so the regressions can be tested directly.
6
6
  */
7
7
 
8
- import { existsSync, readdirSync } from "node:fs";
8
+ import { existsSync } from "node:fs";
9
+ import { detectProjectState } from "./detection.js";
9
10
  import { gsdRoot } from "./paths.js";
10
- import { join } from "node:path";
11
- import type { GSDState, Phase } from "./types.js";
12
11
 
13
12
  export type HealthWidgetProjectState = "none" | "initialized" | "active";
14
13
 
@@ -20,75 +19,19 @@ export interface HealthWidgetData {
20
19
  environmentErrorCount: number;
21
20
  environmentWarningCount: number;
22
21
  lastRefreshed: number;
23
- executionPhase?: Phase;
24
- executionStatus?: string;
25
- executionTarget?: string;
26
- nextAction?: string;
27
- blocker?: string | null;
28
- activeMilestoneId?: string;
29
- activeSliceId?: string;
30
- activeTaskId?: string;
31
- progress?: GSDState["progress"];
32
- eta?: string | null;
33
22
  }
34
23
 
35
24
  export function detectHealthWidgetProjectState(basePath: string): HealthWidgetProjectState {
36
- const root = gsdRoot(basePath);
37
- if (!existsSync(root)) return "none";
25
+ if (!existsSync(gsdRoot(basePath))) return "none";
38
26
 
39
- // Lightweight milestone count avoids the full detectProjectState() scan
40
- // (CI markers, Makefile targets, etc.) that is unnecessary on the 60s refresh.
41
- try {
42
- const milestonesDir = join(root, "milestones");
43
- if (existsSync(milestonesDir)) {
44
- const entries = readdirSync(milestonesDir, { withFileTypes: true });
45
- if (entries.some(e => e.isDirectory())) return "active";
46
- }
47
- } catch { /* non-fatal */ }
48
-
49
- return "initialized";
27
+ const { state } = detectProjectState(basePath);
28
+ return state === "v2-gsd" ? "active" : "initialized";
50
29
  }
51
30
 
52
31
  function formatCost(n: number): string {
53
32
  return n >= 1 ? `$${n.toFixed(2)}` : `${(n * 100).toFixed(1)}¢`;
54
33
  }
55
34
 
56
- function formatProgress(progress?: GSDState["progress"]): string | null {
57
- if (!progress) return null;
58
-
59
- const parts: string[] = [];
60
- parts.push(`M ${progress.milestones.done}/${progress.milestones.total}`);
61
- if (progress.slices) parts.push(`S ${progress.slices.done}/${progress.slices.total}`);
62
- if (progress.tasks) parts.push(`T ${progress.tasks.done}/${progress.tasks.total}`);
63
- return parts.length > 0 ? `Progress: ${parts.join(" · ")}` : null;
64
- }
65
-
66
- function formatEnvironmentSummary(errorCount: number, warningCount: number): string | null {
67
- if (errorCount <= 0 && warningCount <= 0) return null;
68
-
69
- const parts: string[] = [];
70
- if (errorCount > 0) parts.push(`${errorCount} error${errorCount > 1 ? "s" : ""}`);
71
- if (warningCount > 0) parts.push(`${warningCount} warning${warningCount > 1 ? "s" : ""}`);
72
- return `Env: ${parts.join(", ")}`;
73
- }
74
-
75
- function formatBudgetSummary(data: HealthWidgetData): string | null {
76
- if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
77
- const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
78
- return `Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`;
79
- }
80
- if (data.budgetSpent > 0) {
81
- return `Spent: ${formatCost(data.budgetSpent)}`;
82
- }
83
- return null;
84
- }
85
-
86
- function buildExecutionHeadline(data: HealthWidgetData): string {
87
- const status = data.executionStatus ?? "Active project";
88
- const target = data.executionTarget ?? data.blocker ?? "loading status…";
89
- return ` GSD ${status}${target ? ` - ${target}` : ""}`;
90
- }
91
-
92
35
  /**
93
36
  * Build compact health lines for the widget.
94
37
  * Returns a string array suitable for setWidget().
@@ -102,28 +45,33 @@ export function buildHealthLines(data: HealthWidgetData): string[] {
102
45
  return [" GSD Project initialized — run /gsd to continue setup"];
103
46
  }
104
47
 
105
- const lines = [buildExecutionHeadline(data)];
106
- const details: string[] = [];
107
-
108
- const progress = formatProgress(data.progress);
109
- if (progress) details.push(progress);
110
-
111
- if (data.providerIssue) details.push(data.providerIssue);
48
+ const parts: string[] = [];
112
49
 
113
- const environment = formatEnvironmentSummary(
114
- data.environmentErrorCount,
115
- data.environmentWarningCount,
116
- );
117
- if (environment) details.push(environment);
50
+ const totalIssues = data.environmentErrorCount + data.environmentWarningCount + (data.providerIssue ? 1 : 0);
51
+ if (totalIssues === 0) {
52
+ parts.push("● System OK");
53
+ } else if (data.environmentErrorCount > 0 || data.providerIssue?.includes("✗")) {
54
+ parts.push(`✗ ${totalIssues} issue${totalIssues > 1 ? "s" : ""}`);
55
+ } else {
56
+ parts.push(`⚠ ${totalIssues} warning${totalIssues > 1 ? "s" : ""}`);
57
+ }
118
58
 
119
- const budget = formatBudgetSummary(data);
120
- if (budget) details.push(budget);
59
+ if (data.budgetCeiling !== undefined && data.budgetCeiling > 0) {
60
+ const pct = Math.min(100, (data.budgetSpent / data.budgetCeiling) * 100);
61
+ parts.push(`Budget: ${formatCost(data.budgetSpent)}/${formatCost(data.budgetCeiling)} (${pct.toFixed(0)}%)`);
62
+ } else if (data.budgetSpent > 0) {
63
+ parts.push(`Spent: ${formatCost(data.budgetSpent)}`);
64
+ }
121
65
 
122
- if (data.eta) details.push(data.eta);
66
+ if (data.providerIssue) {
67
+ parts.push(data.providerIssue);
68
+ }
123
69
 
124
- if (details.length > 0) {
125
- lines.push(` ${details.join("")}`);
70
+ if (data.environmentErrorCount > 0) {
71
+ parts.push(`Env: ${data.environmentErrorCount} error${data.environmentErrorCount > 1 ? "s" : ""}`);
72
+ } else if (data.environmentWarningCount > 0) {
73
+ parts.push(`Env: ${data.environmentWarningCount} warning${data.environmentWarningCount > 1 ? "s" : ""}`);
126
74
  }
127
75
 
128
- return lines;
76
+ return [` ${parts.join(" │ ")}`];
129
77
  }
@@ -16,7 +16,6 @@ import { loadEffectiveGSDPreferences } from "./preferences.js";
16
16
  import { loadLedgerFromDisk, getProjectTotals } from "./metrics.js";
17
17
  import { describeNextUnit, estimateTimeRemaining, updateSliceProgressCache } from "./auto-dashboard.js";
18
18
  import { projectRoot } from "./commands.js";
19
- import { deriveState, invalidateStateCache } from "./state.js";
20
19
  import {
21
20
  buildHealthLines,
22
21
  detectHealthWidgetProjectState,
@@ -25,7 +24,7 @@ import {
25
24
 
26
25
  // ── Data loader ────────────────────────────────────────────────────────────────
27
26
 
28
- function loadBaseHealthWidgetData(basePath: string): HealthWidgetData {
27
+ function loadHealthWidgetData(basePath: string): HealthWidgetData {
29
28
  let budgetCeiling: number | undefined;
30
29
  let budgetSpent = 0;
31
30
  let providerIssue: string | null = null;
@@ -69,90 +68,6 @@ function loadBaseHealthWidgetData(basePath: string): HealthWidgetData {
69
68
  };
70
69
  }
71
70
 
72
- function compactText(text: string, max = 64): string {
73
- const trimmed = text.replace(/\s+/g, " ").trim();
74
- if (trimmed.length <= max) return trimmed;
75
- return `${trimmed.slice(0, max - 1).trimEnd()}…`;
76
- }
77
-
78
- function summarizeExecutionStatus(state: GSDState): string {
79
- switch (state.phase) {
80
- case "blocked": return "Blocked";
81
- case "paused": return "Paused";
82
- case "complete": return "Complete";
83
- case "executing": return "Executing";
84
- case "planning": return "Planning";
85
- case "pre-planning": return "Pre-planning";
86
- case "summarizing": return "Summarizing";
87
- case "validating-milestone": return "Validating";
88
- case "completing-milestone": return "Completing";
89
- case "needs-discussion": return "Needs discussion";
90
- case "replanning-slice": return "Replanning";
91
- default: return "Active";
92
- }
93
- }
94
-
95
- function summarizeExecutionTarget(state: GSDState): string {
96
- switch (state.phase) {
97
- case "needs-discussion":
98
- return state.activeMilestone ? `Discuss ${state.activeMilestone.id}` : "Discuss milestone draft";
99
- case "pre-planning":
100
- return state.activeMilestone ? `Plan ${state.activeMilestone.id}` : "Research & plan milestone";
101
- case "planning":
102
- return state.activeSlice ? `Plan ${state.activeSlice.id}` : "Plan next slice";
103
- case "executing":
104
- return state.activeTask ? `Execute ${state.activeTask.id}` : "Execute next task";
105
- case "summarizing":
106
- return state.activeSlice ? `Complete ${state.activeSlice.id}` : "Complete current slice";
107
- case "validating-milestone":
108
- return state.activeMilestone ? `Validate ${state.activeMilestone.id}` : "Validate milestone";
109
- case "completing-milestone":
110
- return state.activeMilestone ? `Complete ${state.activeMilestone.id}` : "Complete milestone";
111
- case "replanning-slice":
112
- return state.activeSlice ? `Replan ${state.activeSlice.id}` : "Replan current slice";
113
- case "blocked":
114
- return `waiting on ${compactText(state.blockers[0] ?? state.nextAction, 56)}`;
115
- case "paused":
116
- return compactText(state.nextAction || "waiting to resume", 56);
117
- case "complete":
118
- return "All milestones complete";
119
- default:
120
- return compactText(describeNextUnit(state).label, 56);
121
- }
122
- }
123
-
124
- async function enrichHealthWidgetData(basePath: string, baseData: HealthWidgetData): Promise<HealthWidgetData> {
125
- if (baseData.projectState !== "active") return baseData;
126
-
127
- try {
128
- invalidateStateCache();
129
- const state = await deriveState(basePath);
130
-
131
- if (state.activeMilestone) {
132
- // Warm the slice-progress cache so estimateTimeRemaining() has data
133
- updateSliceProgressCache(basePath, state.activeMilestone.id, state.activeSlice?.id);
134
- }
135
-
136
- return {
137
- ...baseData,
138
- executionPhase: state.phase,
139
- executionStatus: summarizeExecutionStatus(state),
140
- executionTarget: summarizeExecutionTarget(state),
141
- nextAction: state.nextAction,
142
- blocker: state.blockers[0] ?? null,
143
- activeMilestoneId: state.activeMilestone?.id,
144
- activeSliceId: state.activeSlice?.id,
145
- activeTaskId: state.activeTask?.id,
146
- progress: state.progress,
147
- eta: state.phase === "blocked" || state.phase === "paused" || state.phase === "complete"
148
- ? null
149
- : estimateTimeRemaining(),
150
- };
151
- } catch {
152
- return baseData;
153
- }
154
- }
155
-
156
71
  // ── Widget init ────────────────────────────────────────────────────────────────
157
72
 
158
73
  const REFRESH_INTERVAL_MS = 60_000;
@@ -167,7 +82,7 @@ export function initHealthWidget(ctx: ExtensionContext): void {
167
82
  const basePath = projectRoot();
168
83
 
169
84
  // String-array fallback — used in RPC mode (factory is a no-op there)
170
- const initialData = loadBaseHealthWidgetData(basePath);
85
+ const initialData = loadHealthWidgetData(basePath);
171
86
  ctx.ui.setWidget("gsd-health", buildHealthLines(initialData), { placement: "belowEditor" });
172
87
 
173
88
  // Factory-based widget for TUI mode — replaces the string-array above
@@ -180,8 +95,7 @@ export function initHealthWidget(ctx: ExtensionContext): void {
180
95
  if (refreshInFlight) return;
181
96
  refreshInFlight = true;
182
97
  try {
183
- const baseData = loadBaseHealthWidgetData(basePath);
184
- data = await enrichHealthWidgetData(basePath, baseData);
98
+ data = loadHealthWidgetData(basePath);
185
99
  cachedLines = undefined;
186
100
  _tui.requestRender();
187
101
  } catch { /* non-fatal */ } finally {
@@ -92,6 +92,23 @@ function warnDeprecatedAgentInstructions(): void {
92
92
  // ── Depth verification state ──────────────────────────────────────────────
93
93
  let depthVerificationDone = false;
94
94
 
95
+ // ── DB lazy-open helper ───────────────────────────────────────────────────
96
+ // In manual sessions (no auto-mode), the DB is never opened by bootstrapAutoSession.
97
+ // This helper ensures the DB is lazily opened on first tool call that needs it.
98
+ async function ensureDbOpen(): Promise<boolean> {
99
+ try {
100
+ const db = await import("./gsd-db.js");
101
+ if (db.isDbAvailable()) return true;
102
+ const dbPath = join(process.cwd(), ".gsd", "gsd.db");
103
+ if (existsSync(dbPath)) {
104
+ return db.openDatabase(dbPath);
105
+ }
106
+ return false;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
95
112
  // ── Queue phase tracking ──────────────────────────────────────────────────
96
113
  // When true, the LLM is in a queue flow writing CONTEXT.md files.
97
114
  // The write-gate applies during queue flows just like discussion flows.
@@ -300,12 +317,8 @@ export default function (pi: ExtensionAPI) {
300
317
  when_context: Type.Optional(Type.String({ description: "When/context for the decision (e.g. milestone ID)" })),
301
318
  }),
302
319
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
303
- // Check DB availability
304
- let dbAvailable = false;
305
- try {
306
- const db = await import("./gsd-db.js");
307
- dbAvailable = db.isDbAvailable();
308
- } catch { /* dynamic import failed */ }
320
+ // Ensure DB is open (lazy-open on first tool call in manual sessions)
321
+ const dbAvailable = await ensureDbOpen();
309
322
 
310
323
  if (!dbAvailable) {
311
324
  return {
@@ -367,11 +380,7 @@ export default function (pi: ExtensionAPI) {
367
380
  supporting_slices: Type.Optional(Type.String({ description: "Supporting slices" })),
368
381
  }),
369
382
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
370
- let dbAvailable = false;
371
- try {
372
- const db = await import("./gsd-db.js");
373
- dbAvailable = db.isDbAvailable();
374
- } catch { /* dynamic import failed */ }
383
+ const dbAvailable = await ensureDbOpen();
375
384
 
376
385
  if (!dbAvailable) {
377
386
  return {
@@ -441,11 +450,7 @@ export default function (pi: ExtensionAPI) {
441
450
  content: Type.String({ description: "The full markdown content of the artifact" }),
442
451
  }),
443
452
  async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
444
- let dbAvailable = false;
445
- try {
446
- const db = await import("./gsd-db.js");
447
- dbAvailable = db.isDbAvailable();
448
- } catch { /* dynamic import failed */ }
453
+ const dbAvailable = await ensureDbOpen();
449
454
 
450
455
  if (!dbAvailable) {
451
456
  return {
@@ -6,11 +6,13 @@
6
6
  * symlink replaces the original directory so all paths remain valid.
7
7
  */
8
8
 
9
+ import { execFileSync } from "node:child_process";
9
10
  import { existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, cpSync, rmSync, symlinkSync } from "node:fs";
10
11
  import { join } from "node:path";
11
12
  import { externalGsdRoot } from "./repo-identity.js";
12
13
  import { getErrorMessage } from "./error-utils.js";
13
14
  import { hasGitTrackedGsdFiles } from "./gitignore.js";
15
+ import { GIT_NO_PROMPT_ENV } from "./git-constants.js";
14
16
 
15
17
  export interface MigrationResult {
16
18
  migrated: boolean;
@@ -144,7 +146,22 @@ export function migrateToExternalState(basePath: string): MigrationResult {
144
146
  return { migrated: false, error: `Migration verification failed: ${getErrorMessage(verifyErr)}` };
145
147
  }
146
148
 
147
- // Remove .gsd.migrating only after symlink is verified
149
+ // Clean the git index — any .gsd/* files tracked before migration now
150
+ // sit behind the symlink and git can't follow it, causing them to show
151
+ // as deleted. Remove them from the index so the working tree stays clean.
152
+ // --ignore-unmatch makes this a no-op on fresh projects with no tracked .gsd/.
153
+ try {
154
+ execFileSync("git", ["rm", "-r", "--cached", "--ignore-unmatch", ".gsd"], {
155
+ cwd: basePath,
156
+ stdio: ["ignore", "pipe", "ignore"],
157
+ env: GIT_NO_PROMPT_ENV,
158
+ timeout: 10_000,
159
+ });
160
+ } catch {
161
+ // Non-fatal — git may be unavailable or nothing was tracked
162
+ }
163
+
164
+ // Remove .gsd.migrating only after symlink is verified and index is clean
148
165
  rmSync(migratingPath, { recursive: true, force: true });
149
166
 
150
167
  return { migrated: true };
@@ -671,6 +671,43 @@ export function nativeAddAll(basePath: string): void {
671
671
  gitFileExec(basePath, ["add", "-A"]);
672
672
  }
673
673
 
674
+ /**
675
+ * Stage all files with pathspec exclusions (git add -A -- ':!pattern' ...).
676
+ * Excluded paths are never hashed by git, preventing hangs on large
677
+ * untracked artifact trees (57GB+, 11K+ files). See #1605.
678
+ *
679
+ * Falls back to plain `git add -A` when no exclusions are provided.
680
+ * Always uses the CLI path (not libgit2) because libgit2's add_all
681
+ * does not support pathspec exclusion syntax.
682
+ *
683
+ * When excluded paths are already covered by .gitignore, git may exit
684
+ * with code 1 and an "ignored by .gitignore" warning. This is harmless
685
+ * (the staging succeeds for all non-ignored files) and is suppressed.
686
+ */
687
+ export function nativeAddAllWithExclusions(basePath: string, exclusions: readonly string[]): void {
688
+ if (exclusions.length === 0) {
689
+ nativeAddAll(basePath);
690
+ return;
691
+ }
692
+ const pathspecs = exclusions.map(e => `:!${e}`);
693
+ try {
694
+ execFileSync("git", ["add", "-A", "--", ...pathspecs], {
695
+ cwd: basePath,
696
+ stdio: ["ignore", "pipe", "pipe"],
697
+ encoding: "utf-8",
698
+ env: GIT_NO_PROMPT_ENV,
699
+ });
700
+ } catch (err: unknown) {
701
+ // git exits 1 when pathspec exclusions reference paths already covered
702
+ // by .gitignore. The staging itself succeeds — only suppress that case.
703
+ const stderr = (err as { stderr?: string })?.stderr ?? "";
704
+ if (stderr.includes("ignored by one of your .gitignore files")) {
705
+ return;
706
+ }
707
+ throw new GSDError(GSD_GIT_ERROR, `git add -A with exclusions failed in ${basePath}: ${getErrorMessage(err)}`);
708
+ }
709
+ }
710
+
674
711
  /**
675
712
  * Stage specific files.
676
713
  * Native: libgit2 index add.
@@ -356,6 +356,10 @@ export function milestonesDir(basePath: string): string {
356
356
  return join(gsdRoot(basePath), "milestones");
357
357
  }
358
358
 
359
+ export function resolveRuntimeFile(basePath: string): string {
360
+ return join(gsdRoot(basePath), "RUNTIME.md");
361
+ }
362
+
359
363
  export function resolveGsdRootFile(basePath: string, key: GSDRootFileKey): string {
360
364
  const root = gsdRoot(basePath);
361
365
  const canonical = join(root, GSD_ROOT_FILES[key]);
@@ -20,6 +20,7 @@ import type {
20
20
  ReactiveExecutionConfig,
21
21
  } from "./types.js";
22
22
  import type { DynamicRoutingConfig } from "./model-router.js";
23
+ import type { GitHubSyncConfig } from "../github-sync/types.js";
23
24
 
24
25
  // ─── Workflow Modes ──────────────────────────────────────────────────────────
25
26
 
@@ -86,6 +87,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
86
87
  "context_selection",
87
88
  "widget_mode",
88
89
  "reactive_execution",
90
+ "github",
89
91
  ]);
90
92
 
91
93
  /** Canonical list of all dispatch unit types. */
@@ -215,6 +217,8 @@ export interface GSDPreferences {
215
217
  widget_mode?: "full" | "small" | "min" | "off";
216
218
  /** Reactive (graph-derived parallel) task execution within slices. Disabled by default. */
217
219
  reactive_execution?: ReactiveExecutionConfig;
220
+ /** GitHub sync configuration. Opt-in: syncs GSD events to GitHub Issues, Milestones, and PRs. */
221
+ github?: GitHubSyncConfig;
218
222
  }
219
223
 
220
224
  export interface LoadedGSDPreferences {
@@ -696,5 +696,55 @@ export function validatePreferences(preferences: GSDPreferences): {
696
696
  }
697
697
  }
698
698
 
699
+ // ─── GitHub Sync ────────────────────────────────────────────────────────
700
+ if (preferences.github !== undefined) {
701
+ if (typeof preferences.github === "object" && preferences.github !== null) {
702
+ const gh = preferences.github as unknown as Record<string, unknown>;
703
+ const validGh: Record<string, unknown> = {};
704
+
705
+ if (gh.enabled !== undefined) {
706
+ if (typeof gh.enabled === "boolean") validGh.enabled = gh.enabled;
707
+ else errors.push("github.enabled must be a boolean");
708
+ }
709
+ if (gh.repo !== undefined) {
710
+ if (typeof gh.repo === "string" && gh.repo.includes("/")) validGh.repo = gh.repo;
711
+ else errors.push('github.repo must be a string in "owner/repo" format');
712
+ }
713
+ if (gh.project !== undefined) {
714
+ const p = typeof gh.project === "number" ? gh.project : Number(gh.project);
715
+ if (Number.isFinite(p) && p > 0) validGh.project = Math.floor(p);
716
+ else errors.push("github.project must be a positive number");
717
+ }
718
+ if (gh.labels !== undefined) {
719
+ if (Array.isArray(gh.labels) && gh.labels.every((l: unknown) => typeof l === "string")) {
720
+ validGh.labels = gh.labels;
721
+ } else {
722
+ errors.push("github.labels must be an array of strings");
723
+ }
724
+ }
725
+ if (gh.auto_link_commits !== undefined) {
726
+ if (typeof gh.auto_link_commits === "boolean") validGh.auto_link_commits = gh.auto_link_commits;
727
+ else errors.push("github.auto_link_commits must be a boolean");
728
+ }
729
+ if (gh.slice_prs !== undefined) {
730
+ if (typeof gh.slice_prs === "boolean") validGh.slice_prs = gh.slice_prs;
731
+ else errors.push("github.slice_prs must be a boolean");
732
+ }
733
+
734
+ const knownGhKeys = new Set(["enabled", "repo", "project", "labels", "auto_link_commits", "slice_prs"]);
735
+ for (const key of Object.keys(gh)) {
736
+ if (!knownGhKeys.has(key)) {
737
+ warnings.push(`unknown github key "${key}" — ignored`);
738
+ }
739
+ }
740
+
741
+ if (Object.keys(validGh).length > 0) {
742
+ validated.github = validGh as unknown as import("../github-sync/types.js").GitHubSyncConfig;
743
+ }
744
+ } else {
745
+ errors.push("github must be an object");
746
+ }
747
+ }
748
+
699
749
  return { preferences: validated, errors, warnings };
700
750
  }
@@ -14,7 +14,6 @@ import { existsSync, readFileSync } from "node:fs";
14
14
  import { homedir } from "node:os";
15
15
  import { join } from "node:path";
16
16
 
17
- const gsdHome = process.env.GSD_HOME || join(homedir(), ".gsd");
18
17
  import { gsdRoot } from "./paths.js";
19
18
  import { parse as parseYaml } from "yaml";
20
19
  import type { PostUnitHookConfig, PreDispatchHookConfig, TokenProfile } from "./types.js";
@@ -83,24 +82,36 @@ export {
83
82
 
84
83
  // ─── Path Constants & Getters ───────────────────────────────────────────────
85
84
 
86
- const GLOBAL_PREFERENCES_PATH = join(gsdHome, "preferences.md");
87
- const LEGACY_GLOBAL_PREFERENCES_PATH = join(homedir(), ".pi", "agent", "gsd-preferences.md");
85
+ function gsdHome(): string {
86
+ return process.env.GSD_HOME || join(homedir(), ".gsd");
87
+ }
88
+
89
+ function globalPreferencesPath(): string {
90
+ return join(gsdHome(), "preferences.md");
91
+ }
92
+
93
+ function legacyGlobalPreferencesPath(): string {
94
+ return join(homedir(), ".pi", "agent", "gsd-preferences.md");
95
+ }
96
+
88
97
  function projectPreferencesPath(): string {
89
98
  return join(gsdRoot(process.cwd()), "preferences.md");
90
99
  }
91
100
  // Bootstrap in gitignore.ts historically created PREFERENCES.md (uppercase) by mistake.
92
101
  // Check uppercase as a fallback so those files aren't silently ignored.
93
- const GLOBAL_PREFERENCES_PATH_UPPERCASE = join(gsdHome, "PREFERENCES.md");
102
+ function globalPreferencesPathUppercase(): string {
103
+ return join(gsdHome(), "PREFERENCES.md");
104
+ }
94
105
  function projectPreferencesPathUppercase(): string {
95
106
  return join(gsdRoot(process.cwd()), "PREFERENCES.md");
96
107
  }
97
108
 
98
109
  export function getGlobalGSDPreferencesPath(): string {
99
- return GLOBAL_PREFERENCES_PATH;
110
+ return globalPreferencesPath();
100
111
  }
101
112
 
102
113
  export function getLegacyGlobalGSDPreferencesPath(): string {
103
- return LEGACY_GLOBAL_PREFERENCES_PATH;
114
+ return legacyGlobalPreferencesPath();
104
115
  }
105
116
 
106
117
  export function getProjectGSDPreferencesPath(): string {
@@ -110,9 +121,9 @@ export function getProjectGSDPreferencesPath(): string {
110
121
  // ─── Loading ────────────────────────────────────────────────────────────────
111
122
 
112
123
  export function loadGlobalGSDPreferences(): LoadedGSDPreferences | null {
113
- return loadPreferencesFile(GLOBAL_PREFERENCES_PATH, "global")
114
- ?? loadPreferencesFile(GLOBAL_PREFERENCES_PATH_UPPERCASE, "global")
115
- ?? loadPreferencesFile(LEGACY_GLOBAL_PREFERENCES_PATH, "global");
124
+ return loadPreferencesFile(globalPreferencesPath(), "global")
125
+ ?? loadPreferencesFile(globalPreferencesPathUppercase(), "global")
126
+ ?? loadPreferencesFile(legacyGlobalPreferencesPath(), "global");
116
127
  }
117
128
 
118
129
  export function loadProjectGSDPreferences(): LoadedGSDPreferences | null {
@@ -271,6 +282,9 @@ function mergePreferences(base: GSDPreferences, override: GSDPreferences): GSDPr
271
282
  context_selection: override.context_selection ?? base.context_selection,
272
283
  auto_visualize: override.auto_visualize ?? base.auto_visualize,
273
284
  auto_report: override.auto_report ?? base.auto_report,
285
+ github: (base.github || override.github)
286
+ ? { ...(base.github ?? {}), ...(override.github ?? {}) } as import("../github-sync/types.js").GitHubSyncConfig
287
+ : undefined,
274
288
  };
275
289
  }
276
290
 
@@ -78,6 +78,11 @@ export function loadPrompt(name: string, vars: Record<string, string> = {}): str
78
78
  templateCache.set(name, content);
79
79
  }
80
80
 
81
+ const effectiveVars = {
82
+ skillActivation: "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.",
83
+ ...vars,
84
+ };
85
+
81
86
  // Check BEFORE substitution: find all {{varName}} placeholders the template
82
87
  // declares and verify every one has a value in vars. Checking after substitution
83
88
  // would also flag {{...}} patterns injected by inlined content (e.g. template
@@ -86,7 +91,7 @@ export function loadPrompt(name: string, vars: Record<string, string> = {}): str
86
91
  if (declared) {
87
92
  const missing = [...new Set(declared)]
88
93
  .map(m => m.slice(2, -2))
89
- .filter(key => !(key in vars));
94
+ .filter(key => !(key in effectiveVars));
90
95
  if (missing.length > 0) {
91
96
  throw new GSDError(
92
97
  GSD_PARSE_ERROR,
@@ -97,7 +102,7 @@ export function loadPrompt(name: string, vars: Record<string, string> = {}): str
97
102
  }
98
103
  }
99
104
 
100
- for (const [key, value] of Object.entries(vars)) {
105
+ for (const [key, value] of Object.entries(effectiveVars)) {
101
106
  content = content.replaceAll(`{{${key}}}`, value);
102
107
  }
103
108
 
@@ -16,7 +16,7 @@ All relevant context has been preloaded below — the roadmap, all slice summari
16
16
 
17
17
  Then:
18
18
  1. Use the **Milestone Summary** output template from the inlined context above
19
- 2. If a `GSD Skill Preferences` block is present in system context, use it to decide which skills to load and follow during completion, without relaxing required verification or artifact rules
19
+ 2. {{skillActivation}}
20
20
  3. Verify each **success criterion** from the milestone definition in `{{roadmapPath}}`. For each criterion, confirm it was met with specific evidence from slice summaries, test results, or observable behavior. List any criterion that was NOT met.
21
21
  4. Verify the milestone's **definition of done** — all slices are `[x]`, all slice summaries exist, and any cross-slice integration points work correctly.
22
22
  5. Validate **requirement status transitions**. For each requirement that changed status during this milestone, confirm the transition is supported by evidence. Requirements can move between Active, Validated, Deferred, Blocked, or Out of Scope — but only with proof.
@@ -20,7 +20,7 @@ All relevant context has been preloaded below — the slice plan, all task summa
20
20
 
21
21
  Then:
22
22
  1. Use the **Slice Summary** and **UAT** output templates from the inlined context above
23
- 2. If a `GSD Skill Preferences` block is present in system context, use it to decide which skills to load and follow during completion, without relaxing required verification or artifact rules
23
+ 2. {{skillActivation}}
24
24
  3. Run all slice-level verification checks defined in the slice plan. All must pass before marking the slice done. If any fail, fix them first.
25
25
  4. If the slice plan includes observability/diagnostic surfaces, confirm they work. Skip this for simple slices that don't have observability sections.
26
26
  5. If `.gsd/REQUIREMENTS.md` exists, update it based on what this slice actually proved. Move requirements between Active, Validated, Deferred, Blocked, or Out of Scope only when the evidence from execution supports that change.
@@ -10,6 +10,8 @@ A researcher explored the codebase and a planner decomposed the work — you are
10
10
 
11
11
  {{overridesSection}}
12
12
 
13
+ {{runtimeContext}}
14
+
13
15
  {{resumeSection}}
14
16
 
15
17
  {{carryForwardSection}}
@@ -26,7 +28,7 @@ A researcher explored the codebase and a planner decomposed the work — you are
26
28
 
27
29
  Then:
28
30
  0. Narrate step transitions, key implementation decisions, and verification outcomes as you work. Keep it terse — one line between tool-call clusters, not between every call — but write complete sentences in user-facing prose, not shorthand notes or scratchpad fragments.
29
- 1. **Load relevant skills before writing code.** Check the `GSD Skill Preferences` block in system context and the `<available_skills>` catalog in your system prompt. For each skill that matches this task's technology stack (e.g., React, Next.js, accessibility, component design), `read` its SKILL.md file now. Skills contain implementation rules and patterns that should guide your code. If no skills match this task, skip this step.
31
+ 1. {{skillActivation}} Follow any activated skills before writing code. If no skills match this task, skip this step.
30
32
  2. Execute the steps in the inlined task plan, adapting minor local mismatches when the surrounding code differs from the planner's snapshot
31
33
  3. Build the real thing. If the task plan says "create login endpoint", build an endpoint that actually authenticates against a real store, not one that returns a hardcoded success response. If the task plan says "create dashboard page", build a page that renders real data from the API, not a component with hardcoded props. Stubs and mocks are for tests, not for the shipped feature.
32
34
  4. Write or update tests as part of execution — tests are verification, not an afterthought. If the slice plan defines test files in its Verification section and this is the first task, create them (they should initially fail).