pi-gsd 1.2.2 → 1.3.1

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 (58) hide show
  1. package/.gsd/extensions/gsd-hooks.ts +550 -211
  2. package/README.md +2 -2
  3. package/package.json +6 -2
  4. package/prompts/gsd-add-backlog.md +6 -0
  5. package/prompts/gsd-add-phase.md +6 -0
  6. package/prompts/gsd-add-tests.md +6 -0
  7. package/prompts/gsd-add-todo.md +6 -0
  8. package/prompts/gsd-audit-milestone.md +6 -0
  9. package/prompts/gsd-audit-uat.md +6 -0
  10. package/prompts/gsd-autonomous.md +6 -0
  11. package/prompts/gsd-check-todos.md +6 -0
  12. package/prompts/gsd-cleanup.md +6 -0
  13. package/prompts/gsd-complete-milestone.md +6 -0
  14. package/prompts/gsd-debug.md +6 -0
  15. package/prompts/gsd-discuss-phase.md +6 -0
  16. package/prompts/gsd-do.md +6 -0
  17. package/prompts/gsd-execute-phase.md +6 -0
  18. package/prompts/gsd-fast.md +6 -0
  19. package/prompts/gsd-forensics.md +6 -0
  20. package/prompts/gsd-insert-phase.md +6 -0
  21. package/prompts/gsd-join-discord.md +6 -0
  22. package/prompts/gsd-list-phase-assumptions.md +6 -0
  23. package/prompts/gsd-list-workspaces.md +6 -0
  24. package/prompts/gsd-manager.md +6 -0
  25. package/prompts/gsd-map-codebase.md +6 -0
  26. package/prompts/gsd-milestone-summary.md +6 -0
  27. package/prompts/gsd-new-milestone.md +6 -0
  28. package/prompts/gsd-new-project.md +6 -0
  29. package/prompts/gsd-new-workspace.md +6 -0
  30. package/prompts/gsd-next.md +6 -0
  31. package/prompts/gsd-note.md +6 -0
  32. package/prompts/gsd-pause-work.md +6 -0
  33. package/prompts/gsd-plan-milestone-gaps.md +6 -0
  34. package/prompts/gsd-plan-phase.md +6 -0
  35. package/prompts/gsd-plant-seed.md +6 -0
  36. package/prompts/gsd-pr-branch.md +6 -0
  37. package/prompts/gsd-profile-user.md +6 -0
  38. package/prompts/gsd-quick.md +6 -0
  39. package/prompts/gsd-reapply-patches.md +6 -0
  40. package/prompts/gsd-remove-phase.md +6 -0
  41. package/prompts/gsd-remove-workspace.md +6 -0
  42. package/prompts/gsd-research-phase.md +6 -0
  43. package/prompts/gsd-resume-work.md +6 -0
  44. package/prompts/gsd-review-backlog.md +6 -0
  45. package/prompts/gsd-review.md +6 -0
  46. package/prompts/gsd-session-report.md +6 -0
  47. package/prompts/gsd-set-profile.md +6 -0
  48. package/prompts/gsd-settings.md +6 -0
  49. package/prompts/gsd-setup-pi.md +6 -0
  50. package/prompts/gsd-ship.md +6 -0
  51. package/prompts/gsd-thread.md +6 -0
  52. package/prompts/gsd-ui-phase.md +6 -0
  53. package/prompts/gsd-ui-review.md +6 -0
  54. package/prompts/gsd-update.md +6 -0
  55. package/prompts/gsd-validate-phase.md +6 -0
  56. package/prompts/gsd-verify-work.md +6 -0
  57. package/prompts/gsd-workstreams.md +6 -0
  58. package/scripts/postinstall.js +265 -250
@@ -23,215 +23,554 @@ import { join } from "node:path";
23
23
  import type { ContextUsage, ExtensionAPI } from "@mariozechner/pi-coding-agent";
24
24
 
25
25
  export default function (pi: ExtensionAPI) {
26
- // ── session_start: GSD update check ──────────────────────────────────────
27
- pi.on("session_start", async (_event, ctx) => {
28
- try {
29
- const cacheDir = join(homedir(), ".pi", "cache");
30
- const cacheFile = join(cacheDir, "gsd-update-check.json");
31
- const CACHE_TTL_SECONDS = 86_400; // 24 hours
32
-
33
- // Show cached update notification if available
34
- if (existsSync(cacheFile)) {
35
- try {
36
- const cache = JSON.parse(readFileSync(cacheFile, "utf8")) as {
37
- update_available?: boolean;
38
- installed?: string;
39
- latest?: string;
40
- checked?: number;
41
- };
42
- const ageSeconds =
43
- Math.floor(Date.now() / 1000) - (cache.checked ?? 0);
44
-
45
- if (cache.update_available && cache.latest) {
46
- ctx.ui.notify(
47
- `GSD update available: ${cache.installed ?? "?"} → ${cache.latest}. Run: npm i -g pi-gsd`,
48
- "info",
49
- );
50
- }
51
-
52
- // Cache is fresh - skip network check
53
- if (ageSeconds < CACHE_TTL_SECONDS) return;
54
- } catch {
55
- // Corrupt cache - fall through to fresh check
56
- }
57
- }
58
-
59
- // Run network check asynchronously after 3 s to avoid blocking startup
60
- setTimeout(() => {
61
- try {
62
- mkdirSync(cacheDir, { recursive: true });
63
-
64
- // Resolve installed version from project or global GSD install
65
- let installed = "0.0.0";
66
- const versionPaths = [
67
- join(ctx.cwd, ".pi", "gsd", "VERSION"),
68
- join(homedir(), ".pi", "gsd", "VERSION"),
69
- ];
70
- for (const vp of versionPaths) {
71
- if (existsSync(vp)) {
72
- try {
73
- installed = readFileSync(vp, "utf8").trim();
74
- break;
75
- } catch {
76
- /* skip unreadable */
77
- }
78
- }
79
- }
80
-
81
- let latest: string | null = null;
82
- try {
83
- latest = execSync("npm view pi-gsd version", {
84
- encoding: "utf8",
85
- timeout: 10_000,
86
- windowsHide: true,
87
- }).trim();
88
- } catch {
89
- /* offline or npm unavailable */
90
- }
91
-
92
- writeFileSync(
93
- cacheFile,
94
- JSON.stringify({
95
- update_available:
96
- latest !== null &&
97
- installed !== "0.0.0" &&
98
- installed !== latest,
99
- installed,
100
- latest: latest ?? "unknown",
101
- checked: Math.floor(Date.now() / 1000),
102
- }),
103
- );
104
- } catch {
105
- /* silent fail */
106
- }
107
- }, 3_000);
108
- } catch {
109
- /* silent fail - never throw from session_start */
110
- }
111
- });
112
-
113
- // ── tool_call: workflow guard (advisory only, never blocking) ────────────
114
- pi.on("tool_call", async (event, ctx) => {
115
- try {
116
- // Only guard write and edit tool calls
117
- if (event.toolName !== "write" && event.toolName !== "edit")
118
- return undefined;
119
-
120
- const filePath = (event.input as { path?: string }).path ?? "";
121
-
122
- // Allow .planning/ edits (GSD state management)
123
- if (filePath.includes(".planning/")) return undefined;
124
-
125
- // Allow common config/docs files that don't need GSD tracking
126
- const allowed = [
127
- /\.gitignore$/,
128
- /\.env/,
129
- /AGENTS\.md$/,
130
- /settings\.json$/,
131
- /gsd-hooks\.ts$/,
132
- ];
133
- if (allowed.some((p) => p.test(filePath))) return undefined;
134
-
135
- // Only activate when GSD project has workflow_guard enabled
136
- const configPath = join(ctx.cwd, ".planning", "config.json");
137
- if (!existsSync(configPath)) return undefined; // No GSD project
138
-
139
- try {
140
- const config = JSON.parse(readFileSync(configPath, "utf8")) as {
141
- hooks?: { workflow_guard?: boolean };
142
- };
143
- if (!config.hooks?.workflow_guard) return undefined; // Guard disabled (default)
144
- } catch {
145
- return undefined;
146
- }
147
-
148
- // Advisory only - never block tool execution
149
- const fileName = filePath.split("/").pop() ?? filePath;
150
- ctx.ui.notify(
151
- `⚠️ GSD: Editing ${fileName} outside a GSD workflow. Consider /gsd-fast or /gsd-quick to maintain state tracking.`,
152
- "info",
153
- );
154
- } catch {
155
- /* silent fail - never block tool execution */
156
- }
157
-
158
- return undefined;
159
- });
160
-
161
- // ── tool_result: context usage monitor ───────────────────────────────────
162
- const WARNING_THRESHOLD = 35; // warn when remaining % ≤ 35
163
- const CRITICAL_THRESHOLD = 25; // critical when remaining % ≤ 25
164
- const DEBOUNCE_CALLS = 5; // minimum tool uses between repeated warnings
165
-
166
- let callsSinceWarn = 0;
167
- let lastLevel: "warning" | "critical" | null = null;
168
-
169
- pi.on("tool_result", async (_event, ctx) => {
170
- try {
171
- const usage: ContextUsage | undefined = ctx.getContextUsage();
172
- if (!usage || usage.percent === null) return undefined;
173
-
174
- const usedPct = Math.round(usage.percent);
175
- const remaining = 100 - usedPct;
176
-
177
- // Below warning threshold - just increment debounce counter
178
- if (remaining > WARNING_THRESHOLD) {
179
- callsSinceWarn++;
180
- return undefined;
181
- }
182
-
183
- // Respect opt-out via project config
184
- const configPath = join(ctx.cwd, ".planning", "config.json");
185
- if (existsSync(configPath)) {
186
- try {
187
- const config = JSON.parse(readFileSync(configPath, "utf8")) as {
188
- hooks?: { context_warnings?: boolean };
189
- };
190
- if (config.hooks?.context_warnings === false) return undefined;
191
- } catch {
192
- /* ignore config errors */
193
- }
194
- }
195
-
196
- const isCritical = remaining <= CRITICAL_THRESHOLD;
197
- const currentLevel: "warning" | "critical" = isCritical
198
- ? "critical"
199
- : "warning";
200
-
201
- callsSinceWarn++;
202
-
203
- // Debounce - allow severity escalation (warning critical bypasses debounce)
204
- const severityEscalated =
205
- currentLevel === "critical" && lastLevel === "warning";
206
- if (
207
- lastLevel !== null &&
208
- callsSinceWarn < DEBOUNCE_CALLS &&
209
- !severityEscalated
210
- ) {
211
- return undefined;
212
- }
213
-
214
- callsSinceWarn = 0;
215
- lastLevel = currentLevel;
216
-
217
- const isGsdActive = existsSync(join(ctx.cwd, ".planning", "STATE.md"));
218
-
219
- let msg: string;
220
- if (isCritical) {
221
- msg = isGsdActive
222
- ? `🔴 CONTEXT CRITICAL: ${usedPct}% used (${remaining}% left). GSD state is in STATE.md. Inform user to run /gsd-pause-work.`
223
- : `🔴 CONTEXT CRITICAL: ${usedPct}% used (${remaining}% left). Inform user context is nearly exhausted.`;
224
- } else {
225
- msg = isGsdActive
226
- ? `⚠️ CONTEXT WARNING: ${usedPct}% used (${remaining}% left). Avoid starting new complex work.`
227
- : `⚠️ CONTEXT WARNING: ${usedPct}% used (${remaining}% left). Context is getting limited.`;
228
- }
229
-
230
- ctx.ui.notify(msg, isCritical ? "error" : "info");
231
- } catch {
232
- /* silent fail - never throw from tool_result */
233
- }
234
-
235
- return undefined;
236
- });
26
+ // ── session_start: GSD update check ──────────────────────────────────────
27
+ pi.on("session_start", async (_event, ctx) => {
28
+ try {
29
+ const cacheDir = join(homedir(), ".pi", "cache");
30
+ const cacheFile = join(cacheDir, "gsd-update-check.json");
31
+ const CACHE_TTL_SECONDS = 86_400; // 24 hours
32
+
33
+ // Show cached update notification if available
34
+ if (existsSync(cacheFile)) {
35
+ try {
36
+ const cache = JSON.parse(readFileSync(cacheFile, "utf8")) as {
37
+ update_available?: boolean;
38
+ installed?: string;
39
+ latest?: string;
40
+ checked?: number;
41
+ };
42
+ const ageSeconds =
43
+ Math.floor(Date.now() / 1000) - (cache.checked ?? 0);
44
+
45
+ if (cache.update_available && cache.latest) {
46
+ ctx.ui.notify(
47
+ `GSD update available: ${cache.installed ?? "?"} → ${cache.latest}. Run: npm i -g pi-gsd`,
48
+ "info",
49
+ );
50
+ }
51
+
52
+ // Cache is fresh - skip network check
53
+ if (ageSeconds < CACHE_TTL_SECONDS) return;
54
+ } catch {
55
+ // Corrupt cache - fall through to fresh check
56
+ }
57
+ }
58
+
59
+ // Run network check asynchronously after 3 s to avoid blocking startup
60
+ setTimeout(() => {
61
+ try {
62
+ mkdirSync(cacheDir, { recursive: true });
63
+
64
+ // Resolve installed version from project or global GSD install
65
+ let installed = "0.0.0";
66
+ const versionPaths = [
67
+ join(ctx.cwd, ".pi", "gsd", "VERSION"),
68
+ join(homedir(), ".pi", "gsd", "VERSION"),
69
+ ];
70
+ for (const vp of versionPaths) {
71
+ if (existsSync(vp)) {
72
+ try {
73
+ installed = readFileSync(vp, "utf8").trim();
74
+ break;
75
+ } catch {
76
+ /* skip unreadable */
77
+ }
78
+ }
79
+ }
80
+
81
+ let latest: string | null = null;
82
+ try {
83
+ latest = execSync("npm view pi-gsd version", {
84
+ encoding: "utf8",
85
+ timeout: 10_000,
86
+ windowsHide: true,
87
+ }).trim();
88
+ } catch {
89
+ /* offline or npm unavailable */
90
+ }
91
+
92
+ writeFileSync(
93
+ cacheFile,
94
+ JSON.stringify({
95
+ update_available:
96
+ latest !== null &&
97
+ installed !== "0.0.0" &&
98
+ installed !== latest,
99
+ installed,
100
+ latest: latest ?? "unknown",
101
+ checked: Math.floor(Date.now() / 1000),
102
+ }),
103
+ );
104
+ } catch {
105
+ /* silent fail */
106
+ }
107
+ }, 3_000);
108
+ } catch {
109
+ /* silent fail - never throw from session_start */
110
+ }
111
+ });
112
+
113
+ // ── tool_call: workflow guard (advisory only, never blocking) ────────────
114
+ pi.on("tool_call", async (event, ctx) => {
115
+ try {
116
+ // Only guard write and edit tool calls
117
+ if (event.toolName !== "write" && event.toolName !== "edit")
118
+ return undefined;
119
+
120
+ const filePath = (event.input as { path?: string }).path ?? "";
121
+
122
+ // Allow .planning/ edits (GSD state management)
123
+ if (filePath.includes(".planning/")) return undefined;
124
+
125
+ // Allow common config/docs files that don't need GSD tracking
126
+ const allowed = [
127
+ /\.gitignore$/,
128
+ /\.env/,
129
+ /AGENTS\.md$/,
130
+ /settings\.json$/,
131
+ /gsd-hooks\.ts$/,
132
+ ];
133
+ if (allowed.some((p) => p.test(filePath))) return undefined;
134
+
135
+ // Only activate when GSD project has workflow_guard enabled
136
+ const configPath = join(ctx.cwd, ".planning", "config.json");
137
+ if (!existsSync(configPath)) return undefined; // No GSD project
138
+
139
+ try {
140
+ const config = JSON.parse(readFileSync(configPath, "utf8")) as {
141
+ hooks?: { workflow_guard?: boolean };
142
+ };
143
+ if (!config.hooks?.workflow_guard) return undefined; // Guard disabled (default)
144
+ } catch {
145
+ return undefined;
146
+ }
147
+
148
+ // Advisory only - never block tool execution
149
+ const fileName = filePath.split("/").pop() ?? filePath;
150
+ ctx.ui.notify(
151
+ `⚠️ GSD: Editing ${fileName} outside a GSD workflow. Consider /gsd-fast or /gsd-quick to maintain state tracking.`,
152
+ "info",
153
+ );
154
+ } catch {
155
+ /* silent fail - never block tool execution */
156
+ }
157
+
158
+ return undefined;
159
+ });
160
+
161
+ // ── Instant commands (zero LLM, deterministic output) ────────────────────
162
+
163
+ // JSON shapes returned by pi-gsd-tools
164
+ interface GsdPhase {
165
+ number: string;
166
+ name: string;
167
+ plans: number;
168
+ summaries: number;
169
+ status: string;
170
+ }
171
+ interface GsdProgress {
172
+ milestone_version: string;
173
+ milestone_name: string;
174
+ phases: GsdPhase[];
175
+ total_plans: number;
176
+ total_summaries: number;
177
+ percent: number;
178
+ }
179
+ interface GsdStats extends GsdProgress {
180
+ phases_completed: number;
181
+ phases_total: number;
182
+ plan_percent: number;
183
+ requirements_total: number;
184
+ requirements_complete: number;
185
+ git_commits: number;
186
+ git_first_commit_date: string;
187
+ last_activity: string;
188
+ }
189
+ interface GsdState {
190
+ milestone: string;
191
+ milestone_name: string;
192
+ status: string;
193
+ last_activity: string;
194
+ progress: {
195
+ total_phases: string;
196
+ completed_phases: string;
197
+ total_plans: string;
198
+ completed_plans: string;
199
+ };
200
+ }
201
+ interface GsdHealth {
202
+ status: string;
203
+ errors: Array<{ code: string; message: string; repair?: string }>;
204
+ warnings: Array<{ code: string; message: string }>;
205
+ }
206
+
207
+ const runJson = <T>(args: string, cwd: string): T | null => {
208
+ try {
209
+ const raw = execSync(
210
+ `pi-gsd-tools ${args} --raw --cwd ${JSON.stringify(cwd)}`,
211
+ { encoding: "utf8", timeout: 10_000, windowsHide: true },
212
+ ).trim();
213
+ return JSON.parse(raw) as T;
214
+ } catch {
215
+ return null;
216
+ }
217
+ };
218
+
219
+ const bar = (pct: number, width = 20): string => {
220
+ const filled = Math.round((pct / 100) * width);
221
+ return "█".repeat(filled) + "░".repeat(width - filled);
222
+ };
223
+
224
+ const cap = (s: string, max = 42): string =>
225
+ s.length > max ? s.slice(0, max - 1) + "…" : s;
226
+
227
+ /** Derive the next GSD action from phase data no LLM needed. */
228
+ const nextSteps = (phases: GsdPhase[]): string[] => {
229
+ const pending = phases.filter((p) => p.status !== "Complete");
230
+ if (pending.length === 0) {
231
+ return [
232
+ " ✅ All phases complete!",
233
+ " → /gsd-audit-milestone Review before archiving",
234
+ " → /gsd-complete-milestone Archive and start next",
235
+ ];
236
+ }
237
+ const next = pending[0];
238
+ const n = next.number;
239
+ const lines: string[] = [` ⏳ Phase ${n}: ${cap(next.name)}`];
240
+ if (next.plans === 0) {
241
+ lines.push(` → /gsd-discuss-phase ${n} Gather context first`);
242
+ lines.push(` → /gsd-plan-phase ${n} Jump straight to planning`);
243
+ } else if (next.summaries < next.plans) {
244
+ lines.push(` → /gsd-execute-phase ${n} ${next.summaries}/${next.plans} plans done`);
245
+ } else {
246
+ lines.push(` → /gsd-verify-work ${n} All plans done, verify UAT`);
247
+ }
248
+ lines.push(` → /gsd-next Auto-advance`);
249
+ if (pending.length > 1) {
250
+ lines.push(
251
+ ` (+ ${pending.length - 1} more phase${pending.length > 2 ? "s" : ""} pending)`,
252
+ );
253
+ }
254
+ return lines;
255
+ };
256
+
257
+ const formatProgress = (cwd: string): { text: string; data: GsdProgress | null } => {
258
+ const data = runJson<GsdProgress>("progress json", cwd);
259
+ if (!data)
260
+ return { text: "❌ No GSD project found. Run /gsd-new-project to initialise.", data: null };
261
+
262
+ const done = data.phases.filter((p) => p.status === "Complete").length;
263
+ const total = data.phases.length;
264
+ const phasePct = total > 0 ? Math.round((done / total) * 100) : 0;
265
+ const planPct =
266
+
267
+ const lines = [
268
+ `━━ GSD Progress ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
269
+ `📋 ${data.milestone_name} (${data.milestone_version})`,
270
+ ``,
271
+ `Phases ${bar(phasePct)} ${done}/${total} (${phasePct}%)`,
272
+ `Plans ${bar(planPct)} ${data.total_summaries}/${data.total_plans} (${planPct}%)`,
273
+ ``,
274
+ `Next steps:`,
275
+ ...nextSteps(data.phases),
276
+ ``,
277
+ `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
278
+ ];
279
+ return { text: lines.join("\n"), data };
280
+ };
281
+
282
+ const formatStats = (cwd: string): { text: string; data: GsdStats | null } => {
283
+ const data = runJson<GsdStats>("stats json", cwd);
284
+ if (!data)
285
+ return { text: "❌ No GSD project found. Run /gsd-new-project to initialise.", data: null };
286
+
287
+ const reqPct =
288
+ data.requirements_total > 0
289
+ ? Math.round(
290
+ (data.requirements_complete / data.requirements_total) * 100,
291
+ )
292
+ : 0;
293
+
294
+ const lines = [
295
+ `━━ GSD Stats ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
296
+ `📋 ${data.milestone_name} (${data.milestone_version})`,
297
+ ``,
298
+ `Phases ${bar(data.percent)} ${data.phases_completed}/${data.phases_total} (${data.percent}%)`,
299
+ `Plans ${bar(data.plan_percent)} ${data.total_summaries}/${data.total_plans} (${data.plan_percent}%)`,
300
+ `Reqs ${bar(reqPct)} ${data.requirements_complete}/${data.requirements_total} (${reqPct}%)`,
301
+ ``,
302
+ `🗂 Git commits: ${data.git_commits}`,
303
+ `📅 Started: ${data.git_first_commit_date}`,
304
+ `📅 Last activity: ${data.last_activity}`,
305
+ ``,
306
+ `Next steps:`,
307
+ ...nextSteps(data.phases),
308
+ ``,
309
+ `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
310
+ ];
311
+ return { text: lines.join("\n"), data };
312
+ };
313
+
314
+ const formatHealth = (cwd: string, repair: boolean): string => {
315
+ const data = runJson<GsdHealth>(
316
+ `validate health${repair ? " --repair" : ""}`,
317
+ cwd,
318
+ );
319
+ if (!data)
320
+ return "❌ No GSD project found. Run /gsd-new-project to initialise.";
321
+
322
+ const icon =
323
+ data.status === "ok" ? "✅" : data.status === "broken" ? "❌" : "⚠️";
324
+ const lines = [
325
+ `━━ GSD Health ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
326
+ `${icon} Status: ${data.status.toUpperCase()}`,
327
+ ];
328
+
329
+ if (data.errors?.length) {
330
+ lines.push(``, `Errors (${data.errors.length}):`);
331
+ for (const e of data.errors) {
332
+ lines.push(` ✗ [${e.code}] ${e.message}`);
333
+ if (e.repair) lines.push(` fix: ${e.repair}`);
334
+ }
335
+ }
336
+ if (data.warnings?.length) {
337
+ lines.push(``, `Warnings (${data.warnings.length}):`);
338
+ for (const w of data.warnings) {
339
+ lines.push(` ⚠ [${w.code}] ${w.message}`);
340
+ }
341
+ }
342
+ if (data.status !== "ok" && !repair) {
343
+ lines.push(``, ` → /gsd-health --repair Auto-fix all issues`);
344
+ }
345
+ lines.push(``, `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
346
+ return lines.join("\n");
347
+ };
348
+
349
+ /** Derive the suggested next command string from phase data. */
350
+ const nextCommand = (phases: GsdPhase[]): string | null => {
351
+ const pending = phases.filter((p) => p.status !== "Complete");
352
+ if (pending.length === 0) return "/gsd-audit-milestone";
353
+ const next = pending[0];
354
+ const n = next.number;
355
+ if (next.plans === 0) return `/gsd-discuss-phase ${n}`;
356
+ if (next.summaries < next.plans) return `/gsd-execute-phase ${n}`;
357
+ return `/gsd-verify-work ${n}`;
358
+ };
359
+
360
+ pi.registerCommand("gsd-progress", {
361
+ description: "Show project progress with next steps (instant)",
362
+ handler: async (_args, ctx) => {
363
+ const { text, data } = formatProgress(ctx.cwd);
364
+ ctx.ui.notify(text, "info");
365
+ // Pivot affordance: pre-fill the editor with the most relevant next action
366
+ // so the user can run it, modify it, or just type something else entirely
367
+ if (data) {
368
+ const cmd = nextCommand(data.phases);
369
+ if (cmd) ctx.ui.setEditorText(cmd);
370
+ }
371
+ },
372
+ });
373
+
374
+ pi.registerCommand("gsd-stats", {
375
+ description: "Show project statistics (instant)",
376
+ handler: async (_args, ctx) => {
377
+ const { text, data } = formatStats(ctx.cwd);
378
+ ctx.ui.notify(text, "info");
379
+ if (data) {
380
+ const cmd = nextCommand(data.phases);
381
+ if (cmd) ctx.ui.setEditorText(cmd);
382
+ }
383
+ },
384
+ });
385
+
386
+ pi.registerCommand("gsd-health", {
387
+ description: "Check .planning/ integrity (instant)",
388
+ handler: async (args, ctx) => {
389
+ ctx.ui.notify(formatHealth(ctx.cwd, !!args?.includes("--repair")), "info");
390
+ },
391
+ getArgumentCompletions: (prefix) => {
392
+ const options = [{ value: "--repair", label: "--repair Auto-fix issues" }];
393
+ return options.filter((o) => o.value.startsWith(prefix));
394
+ },
395
+ });
396
+
397
+ pi.registerCommand("gsd-next", {
398
+ description: "Auto-advance to the next GSD action (instant, no LLM)",
399
+ handler: async (_args, ctx) => {
400
+ const data = runJson<GsdProgress>("progress json", ctx.cwd);
401
+ if (!data) {
402
+ ctx.ui.notify(
403
+ "❌ No GSD project found. Run /gsd-new-project to initialise.",
404
+ "error",
405
+ );
406
+ ctx.ui.setEditorText("/gsd-new-project");
407
+ return;
408
+ }
409
+
410
+ const pending = data.phases.filter((p) => p.status !== "Complete");
411
+
412
+ if (pending.length === 0) {
413
+ ctx.ui.notify(
414
+ [
415
+ `━━ GSD Next ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
416
+ `✅ All phases complete!`,
417
+ `→ /gsd-audit-milestone`,
418
+ `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
419
+ ].join("\n"),
420
+ "info",
421
+ );
422
+ ctx.ui.setEditorText("/gsd-audit-milestone");
423
+ return;
424
+ }
425
+
426
+ const next = pending[0];
427
+ const n = next.number;
428
+ let action: string;
429
+ let reason: string;
430
+
431
+ if (next.plans === 0) {
432
+ action = `/gsd-discuss-phase ${n}`;
433
+ reason = `Phase ${n} has no plans yet — start with discussion`;
434
+ } else if (next.summaries < next.plans) {
435
+ action = `/gsd-execute-phase ${n}`;
436
+ reason = `Phase ${n}: ${next.summaries}/${next.plans} plans done — continue execution`;
437
+ } else {
438
+ action = `/gsd-verify-work ${n}`;
439
+ reason = `Phase ${n}: all plans done — verify UAT`;
440
+ }
441
+
442
+ ctx.ui.notify(
443
+ [
444
+ `━━ GSD Next ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
445
+ `⏩ ${reason}`,
446
+ `→ ${action}`,
447
+ ...(pending.length > 1
448
+ ? [` (${pending.length - 1} more phase${pending.length > 2 ? "s" : ""} pending after this)`]
449
+ : []),
450
+ `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
451
+ ].join("\n"),
452
+ "info",
453
+ );
454
+ ctx.ui.setEditorText(action);
455
+ },
456
+ });
457
+
458
+ pi.registerCommand("gsd-help", {
459
+ description: "List all GSD commands (instant)",
460
+ handler: async (_args, ctx) => {
461
+ ctx.ui.notify(
462
+ [
463
+ "━━ GSD Commands ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
464
+ "Lifecycle:",
465
+ " /gsd-new-project Initialise project",
466
+ " /gsd-new-milestone Start next milestone",
467
+ " /gsd-discuss-phase N Discuss before planning",
468
+ " /gsd-plan-phase N Create phase plan",
469
+ " /gsd-execute-phase N Execute phase",
470
+ " /gsd-verify-work N UAT testing",
471
+ " /gsd-validate-phase N Validate completion",
472
+ " /gsd-next Auto-advance",
473
+ " /gsd-autonomous Run all phases",
474
+ "",
475
+ "Quick:",
476
+ " /gsd-quick <task> Tracked ad-hoc task",
477
+ " /gsd-fast <task> Inline, no subagents",
478
+ " /gsd-do <text> Route automatically",
479
+ " /gsd-debug Debug session",
480
+ "",
481
+ "Instant (no LLM):",
482
+ " /gsd-progress Progress + next steps",
483
+ " /gsd-stats Full statistics",
484
+ " /gsd-health [--repair] .planning/ integrity",
485
+ " /gsd-help This list",
486
+ "",
487
+ "Management:",
488
+ " /gsd-setup-pi Wire pi extension",
489
+ " /gsd-set-profile <p> quality|balanced|budget",
490
+ " /gsd-settings Workflow toggles",
491
+ " /gsd-progress Roadmap overview",
492
+ "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━",
493
+ ].join("\n"),
494
+ "info",
495
+ );
496
+ },
497
+ });
498
+
499
+
500
+ // ── tool_result: context usage monitor ───────────────────────────────────
501
+ const WARNING_THRESHOLD = 35; // warn when remaining % ≤ 35
502
+ const CRITICAL_THRESHOLD = 25; // critical when remaining % ≤ 25
503
+ const DEBOUNCE_CALLS = 5; // minimum tool uses between repeated warnings
504
+
505
+ let callsSinceWarn = 0;
506
+ let lastLevel: "warning" | "critical" | null = null;
507
+
508
+ pi.on("tool_result", async (_event, ctx) => {
509
+ try {
510
+ const usage: ContextUsage | undefined = ctx.getContextUsage();
511
+ if (!usage || usage.percent === null) return undefined;
512
+
513
+ const usedPct = Math.round(usage.percent);
514
+ const remaining = 100 - usedPct;
515
+
516
+ // Below warning threshold - just increment debounce counter
517
+ if (remaining > WARNING_THRESHOLD) {
518
+ callsSinceWarn++;
519
+ return undefined;
520
+ }
521
+
522
+ // Respect opt-out via project config
523
+ const configPath = join(ctx.cwd, ".planning", "config.json");
524
+ if (existsSync(configPath)) {
525
+ try {
526
+ const config = JSON.parse(readFileSync(configPath, "utf8")) as {
527
+ hooks?: { context_warnings?: boolean };
528
+ };
529
+ if (config.hooks?.context_warnings === false) return undefined;
530
+ } catch {
531
+ /* ignore config errors */
532
+ }
533
+ }
534
+
535
+ const isCritical = remaining <= CRITICAL_THRESHOLD;
536
+ const currentLevel: "warning" | "critical" = isCritical
537
+ ? "critical"
538
+ : "warning";
539
+
540
+ callsSinceWarn++;
541
+
542
+ // Debounce - allow severity escalation (warning → critical bypasses debounce)
543
+ const severityEscalated =
544
+ currentLevel === "critical" && lastLevel === "warning";
545
+ if (
546
+ lastLevel !== null &&
547
+ callsSinceWarn < DEBOUNCE_CALLS &&
548
+ !severityEscalated
549
+ ) {
550
+ return undefined;
551
+ }
552
+
553
+ callsSinceWarn = 0;
554
+ lastLevel = currentLevel;
555
+
556
+ const isGsdActive = existsSync(join(ctx.cwd, ".planning", "STATE.md"));
557
+
558
+ let msg: string;
559
+ if (isCritical) {
560
+ msg = isGsdActive
561
+ ? `🔴 CONTEXT CRITICAL: ${usedPct}% used (${remaining}% left). GSD state is in STATE.md. Inform user to run /gsd-pause-work.`
562
+ : `🔴 CONTEXT CRITICAL: ${usedPct}% used (${remaining}% left). Inform user context is nearly exhausted.`;
563
+ } else {
564
+ msg = isGsdActive
565
+ ? `⚠️ CONTEXT WARNING: ${usedPct}% used (${remaining}% left). Avoid starting new complex work.`
566
+ : `⚠️ CONTEXT WARNING: ${usedPct}% used (${remaining}% left). Context is getting limited.`;
567
+ }
568
+
569
+ ctx.ui.notify(msg, isCritical ? "error" : "info");
570
+ } catch {
571
+ /* silent fail - never throw from tool_result */
572
+ }
573
+
574
+ return undefined;
575
+ });
237
576
  }