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.
- package/.gsd/extensions/gsd-hooks.ts +550 -211
- package/README.md +2 -2
- package/package.json +6 -2
- package/prompts/gsd-add-backlog.md +6 -0
- package/prompts/gsd-add-phase.md +6 -0
- package/prompts/gsd-add-tests.md +6 -0
- package/prompts/gsd-add-todo.md +6 -0
- package/prompts/gsd-audit-milestone.md +6 -0
- package/prompts/gsd-audit-uat.md +6 -0
- package/prompts/gsd-autonomous.md +6 -0
- package/prompts/gsd-check-todos.md +6 -0
- package/prompts/gsd-cleanup.md +6 -0
- package/prompts/gsd-complete-milestone.md +6 -0
- package/prompts/gsd-debug.md +6 -0
- package/prompts/gsd-discuss-phase.md +6 -0
- package/prompts/gsd-do.md +6 -0
- package/prompts/gsd-execute-phase.md +6 -0
- package/prompts/gsd-fast.md +6 -0
- package/prompts/gsd-forensics.md +6 -0
- package/prompts/gsd-insert-phase.md +6 -0
- package/prompts/gsd-join-discord.md +6 -0
- package/prompts/gsd-list-phase-assumptions.md +6 -0
- package/prompts/gsd-list-workspaces.md +6 -0
- package/prompts/gsd-manager.md +6 -0
- package/prompts/gsd-map-codebase.md +6 -0
- package/prompts/gsd-milestone-summary.md +6 -0
- package/prompts/gsd-new-milestone.md +6 -0
- package/prompts/gsd-new-project.md +6 -0
- package/prompts/gsd-new-workspace.md +6 -0
- package/prompts/gsd-next.md +6 -0
- package/prompts/gsd-note.md +6 -0
- package/prompts/gsd-pause-work.md +6 -0
- package/prompts/gsd-plan-milestone-gaps.md +6 -0
- package/prompts/gsd-plan-phase.md +6 -0
- package/prompts/gsd-plant-seed.md +6 -0
- package/prompts/gsd-pr-branch.md +6 -0
- package/prompts/gsd-profile-user.md +6 -0
- package/prompts/gsd-quick.md +6 -0
- package/prompts/gsd-reapply-patches.md +6 -0
- package/prompts/gsd-remove-phase.md +6 -0
- package/prompts/gsd-remove-workspace.md +6 -0
- package/prompts/gsd-research-phase.md +6 -0
- package/prompts/gsd-resume-work.md +6 -0
- package/prompts/gsd-review-backlog.md +6 -0
- package/prompts/gsd-review.md +6 -0
- package/prompts/gsd-session-report.md +6 -0
- package/prompts/gsd-set-profile.md +6 -0
- package/prompts/gsd-settings.md +6 -0
- package/prompts/gsd-setup-pi.md +6 -0
- package/prompts/gsd-ship.md +6 -0
- package/prompts/gsd-thread.md +6 -0
- package/prompts/gsd-ui-phase.md +6 -0
- package/prompts/gsd-ui-review.md +6 -0
- package/prompts/gsd-update.md +6 -0
- package/prompts/gsd-validate-phase.md +6 -0
- package/prompts/gsd-verify-work.md +6 -0
- package/prompts/gsd-workstreams.md +6 -0
- 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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
}
|