gsd-pi 2.28.0-dev.e19bf89 → 2.29.0-dev.49d972f

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 (116) hide show
  1. package/dist/cli.js +15 -9
  2. package/dist/resource-loader.js +80 -8
  3. package/dist/resources/extensions/gsd/auto-post-unit.ts +9 -4
  4. package/dist/resources/extensions/gsd/auto-recovery.ts +33 -23
  5. package/dist/resources/extensions/gsd/auto-start.ts +25 -10
  6. package/dist/resources/extensions/gsd/auto-verification.ts +41 -7
  7. package/dist/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
  8. package/dist/resources/extensions/gsd/auto.ts +67 -22
  9. package/dist/resources/extensions/gsd/commands-handlers.ts +3 -11
  10. package/dist/resources/extensions/gsd/commands-logs.ts +536 -0
  11. package/dist/resources/extensions/gsd/commands-prefs-wizard.ts +46 -33
  12. package/dist/resources/extensions/gsd/commands.ts +22 -28
  13. package/dist/resources/extensions/gsd/dashboard-overlay.ts +2 -1
  14. package/dist/resources/extensions/gsd/doctor-types.ts +13 -0
  15. package/dist/resources/extensions/gsd/doctor.ts +2 -6
  16. package/dist/resources/extensions/gsd/export.ts +28 -2
  17. package/dist/resources/extensions/gsd/gsd-db.ts +19 -0
  18. package/dist/resources/extensions/gsd/index.ts +2 -1
  19. package/dist/resources/extensions/gsd/json-persistence.ts +67 -0
  20. package/dist/resources/extensions/gsd/metrics.ts +17 -31
  21. package/dist/resources/extensions/gsd/paths.ts +0 -8
  22. package/dist/resources/extensions/gsd/queue-order.ts +10 -11
  23. package/dist/resources/extensions/gsd/routing-history.ts +13 -17
  24. package/dist/resources/extensions/gsd/session-lock.ts +284 -0
  25. package/dist/resources/extensions/gsd/session-status-io.ts +23 -41
  26. package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  27. package/dist/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
  28. package/dist/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
  29. package/dist/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
  30. package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
  31. package/dist/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
  32. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
  33. package/dist/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
  34. package/dist/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
  35. package/dist/resources/extensions/gsd/types.ts +1 -0
  36. package/dist/resources/extensions/gsd/unit-runtime.ts +16 -13
  37. package/dist/resources/extensions/gsd/verification-evidence.ts +2 -0
  38. package/dist/resources/extensions/gsd/verification-gate.ts +13 -2
  39. package/dist/resources/extensions/remote-questions/discord-adapter.ts +9 -20
  40. package/dist/resources/extensions/remote-questions/http-client.ts +76 -0
  41. package/dist/resources/extensions/remote-questions/notify.ts +1 -2
  42. package/dist/resources/extensions/remote-questions/slack-adapter.ts +11 -18
  43. package/dist/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
  44. package/dist/resources/extensions/remote-questions/types.ts +3 -0
  45. package/dist/resources/extensions/shared/mod.ts +3 -0
  46. package/package.json +6 -3
  47. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  48. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  49. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  50. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  51. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  52. package/packages/pi-coding-agent/dist/core/system-prompt.js +10 -0
  53. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  54. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  55. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +4 -1
  56. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  57. package/packages/pi-coding-agent/package.json +1 -1
  58. package/packages/pi-coding-agent/scripts/copy-assets.cjs +39 -8
  59. package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
  60. package/packages/pi-coding-agent/src/core/system-prompt.ts +11 -0
  61. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +4 -1
  62. package/packages/pi-tui/dist/autocomplete.d.ts +3 -0
  63. package/packages/pi-tui/dist/autocomplete.d.ts.map +1 -1
  64. package/packages/pi-tui/dist/autocomplete.js +14 -0
  65. package/packages/pi-tui/dist/autocomplete.js.map +1 -1
  66. package/packages/pi-tui/src/autocomplete.ts +19 -1
  67. package/pkg/package.json +1 -1
  68. package/src/resources/extensions/gsd/auto-post-unit.ts +9 -4
  69. package/src/resources/extensions/gsd/auto-recovery.ts +33 -23
  70. package/src/resources/extensions/gsd/auto-start.ts +25 -10
  71. package/src/resources/extensions/gsd/auto-verification.ts +41 -7
  72. package/src/resources/extensions/gsd/auto-worktree-sync.ts +21 -6
  73. package/src/resources/extensions/gsd/auto.ts +67 -22
  74. package/src/resources/extensions/gsd/commands-handlers.ts +3 -11
  75. package/src/resources/extensions/gsd/commands-logs.ts +536 -0
  76. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +46 -33
  77. package/src/resources/extensions/gsd/commands.ts +22 -28
  78. package/src/resources/extensions/gsd/dashboard-overlay.ts +2 -1
  79. package/src/resources/extensions/gsd/doctor-types.ts +13 -0
  80. package/src/resources/extensions/gsd/doctor.ts +2 -6
  81. package/src/resources/extensions/gsd/export.ts +28 -2
  82. package/src/resources/extensions/gsd/gsd-db.ts +19 -0
  83. package/src/resources/extensions/gsd/index.ts +2 -1
  84. package/src/resources/extensions/gsd/json-persistence.ts +67 -0
  85. package/src/resources/extensions/gsd/metrics.ts +17 -31
  86. package/src/resources/extensions/gsd/paths.ts +0 -8
  87. package/src/resources/extensions/gsd/queue-order.ts +10 -11
  88. package/src/resources/extensions/gsd/routing-history.ts +13 -17
  89. package/src/resources/extensions/gsd/session-lock.ts +284 -0
  90. package/src/resources/extensions/gsd/session-status-io.ts +23 -41
  91. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  92. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +1 -1
  93. package/src/resources/extensions/gsd/tests/commands-logs.test.ts +241 -0
  94. package/src/resources/extensions/gsd/tests/gsd-inspect.test.ts +1 -1
  95. package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +1 -1
  96. package/src/resources/extensions/gsd/tests/session-lock.test.ts +315 -0
  97. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +55 -0
  98. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +26 -24
  99. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +136 -7
  100. package/src/resources/extensions/gsd/types.ts +1 -0
  101. package/src/resources/extensions/gsd/unit-runtime.ts +16 -13
  102. package/src/resources/extensions/gsd/verification-evidence.ts +2 -0
  103. package/src/resources/extensions/gsd/verification-gate.ts +13 -2
  104. package/src/resources/extensions/remote-questions/discord-adapter.ts +9 -20
  105. package/src/resources/extensions/remote-questions/http-client.ts +76 -0
  106. package/src/resources/extensions/remote-questions/notify.ts +1 -2
  107. package/src/resources/extensions/remote-questions/slack-adapter.ts +11 -18
  108. package/src/resources/extensions/remote-questions/telegram-adapter.ts +8 -20
  109. package/src/resources/extensions/remote-questions/types.ts +3 -0
  110. package/src/resources/extensions/shared/mod.ts +3 -0
  111. package/dist/resources/extensions/gsd/preferences-hooks.ts +0 -10
  112. package/dist/resources/extensions/shared/progress-widget.ts +0 -282
  113. package/dist/resources/extensions/shared/thinking-widget.ts +0 -107
  114. package/src/resources/extensions/gsd/preferences-hooks.ts +0 -10
  115. package/src/resources/extensions/shared/progress-widget.ts +0 -282
  116. package/src/resources/extensions/shared/thinking-widget.ts +0 -107
@@ -0,0 +1,536 @@
1
+ /**
2
+ * /gsd logs — Browse activity logs, debug logs, and metrics.
3
+ *
4
+ * Subcommands:
5
+ * /gsd logs — List recent activity + debug logs
6
+ * /gsd logs <N> — Show summary of activity log #N
7
+ * /gsd logs debug — List debug log files
8
+ * /gsd logs debug <N> — Show debug log summary #N
9
+ * /gsd logs tail [N] — Show last N activity log entries (default 5)
10
+ * /gsd logs clear — Remove old activity and debug logs
11
+ */
12
+
13
+ import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
14
+ import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { gsdRoot } from "./paths.js";
17
+ import { loadJsonFileOrNull } from "./json-persistence.js";
18
+
19
+ // ─── Types ──────────────────────────────────────────────────────────────────
20
+
21
+ interface LogEntry {
22
+ seq: number;
23
+ filename: string;
24
+ unitType: string;
25
+ unitId: string;
26
+ size: number;
27
+ mtime: Date;
28
+ }
29
+
30
+ interface DebugLogEntry {
31
+ filename: string;
32
+ size: number;
33
+ mtime: Date;
34
+ }
35
+
36
+ // ─── Helpers ────────────────────────────────────────────────────────────────
37
+
38
+ function activityDir(basePath: string): string {
39
+ return join(gsdRoot(basePath), "activity");
40
+ }
41
+
42
+ function debugDir(basePath: string): string {
43
+ return join(gsdRoot(basePath), "debug");
44
+ }
45
+
46
+ function listActivityLogs(basePath: string): LogEntry[] {
47
+ const dir = activityDir(basePath);
48
+ if (!existsSync(dir)) return [];
49
+
50
+ const entries: LogEntry[] = [];
51
+ try {
52
+ for (const f of readdirSync(dir)) {
53
+ if (!f.endsWith(".jsonl")) continue;
54
+ // Filename format: {seq}-{unitType}-{unitId}.jsonl
55
+ // unitType is lowercase-with-hyphens (e.g., "execute-task", "complete-slice")
56
+ // unitId starts with M followed by digits (e.g., "M001-S01-T01")
57
+ const match = f.match(/^(\d+)-([\w-]+?)-(M\d[\w-]*)\.jsonl$/);
58
+ if (!match) continue;
59
+
60
+ const filePath = join(dir, f);
61
+ let stat;
62
+ try { stat = statSync(filePath); } catch { continue; }
63
+
64
+ entries.push({
65
+ seq: parseInt(match[1], 10),
66
+ filename: f,
67
+ unitType: match[2],
68
+ unitId: match[3].replace(/-/g, "/"),
69
+ size: stat.size,
70
+ mtime: stat.mtime,
71
+ });
72
+ }
73
+ } catch { /* dir not readable */ }
74
+
75
+ return entries.sort((a, b) => a.seq - b.seq);
76
+ }
77
+
78
+ function listDebugLogs(basePath: string): DebugLogEntry[] {
79
+ const dir = debugDir(basePath);
80
+ if (!existsSync(dir)) return [];
81
+
82
+ const entries: DebugLogEntry[] = [];
83
+ try {
84
+ for (const f of readdirSync(dir)) {
85
+ if (!f.endsWith(".log")) continue;
86
+ const filePath = join(dir, f);
87
+ let stat;
88
+ try { stat = statSync(filePath); } catch { continue; }
89
+ entries.push({ filename: f, size: stat.size, mtime: stat.mtime });
90
+ }
91
+ } catch { /* dir not readable */ }
92
+
93
+ return entries.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
94
+ }
95
+
96
+ function formatSize(bytes: number): string {
97
+ if (bytes < 1024) return `${bytes}B`;
98
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
99
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
100
+ }
101
+
102
+ function formatAge(date: Date): string {
103
+ const ms = Date.now() - date.getTime();
104
+ const mins = Math.floor(ms / 60_000);
105
+ if (mins < 1) return "just now";
106
+ if (mins < 60) return `${mins}m ago`;
107
+ const hrs = Math.floor(mins / 60);
108
+ if (hrs < 24) return `${hrs}h ago`;
109
+ const days = Math.floor(hrs / 24);
110
+ return `${days}d ago`;
111
+ }
112
+
113
+ /**
114
+ * Extract a summary from an activity log JSONL file.
115
+ * Parses the entries to count tool calls, errors, and extract key events.
116
+ */
117
+ function summarizeActivityLog(filePath: string): {
118
+ toolCalls: number;
119
+ errors: number;
120
+ filesWritten: string[];
121
+ commandsRun: Array<{ command: string; failed: boolean }>;
122
+ lastReasoning: string;
123
+ entryCount: number;
124
+ } {
125
+ const result = {
126
+ toolCalls: 0,
127
+ errors: 0,
128
+ filesWritten: new Set<string>(),
129
+ commandsRun: [] as Array<{ command: string; failed: boolean }>,
130
+ lastReasoning: "",
131
+ entryCount: 0,
132
+ };
133
+
134
+ let raw: string;
135
+ try { raw = readFileSync(filePath, "utf-8"); } catch { return { ...result, filesWritten: [] }; }
136
+
137
+ const lines = raw.split("\n").filter(l => l.trim());
138
+ result.entryCount = lines.length;
139
+
140
+ for (const line of lines) {
141
+ let entry: Record<string, unknown>;
142
+ try { entry = JSON.parse(line); } catch { continue; }
143
+
144
+ // Count tool calls
145
+ if (entry.type === "toolCall" || (entry.role === "assistant" && entry.content && Array.isArray(entry.content))) {
146
+ if (entry.type === "toolCall") {
147
+ result.toolCalls++;
148
+ const name = entry.name as string | undefined;
149
+ const args = entry.arguments as Record<string, unknown> | undefined;
150
+
151
+ if (name === "write" || name === "edit") {
152
+ const path = args?.file_path as string | undefined;
153
+ if (path) result.filesWritten.add(path);
154
+ }
155
+ if (name === "bash") {
156
+ const cmd = args?.command as string | undefined;
157
+ if (cmd) result.commandsRun.push({ command: cmd.slice(0, 80), failed: false });
158
+ }
159
+ }
160
+ }
161
+
162
+ // Count errors
163
+ if (entry.role === "toolResult" && entry.isError) {
164
+ result.errors++;
165
+ // Mark last command as failed
166
+ if (result.commandsRun.length > 0) {
167
+ result.commandsRun[result.commandsRun.length - 1].failed = true;
168
+ }
169
+ }
170
+
171
+ // Track assistant reasoning
172
+ if (entry.role === "assistant" && typeof entry.content === "string") {
173
+ result.lastReasoning = entry.content.slice(0, 200);
174
+ }
175
+ }
176
+
177
+ return {
178
+ ...result,
179
+ filesWritten: [...result.filesWritten],
180
+ };
181
+ }
182
+
183
+ /**
184
+ * Extract summary events from a debug log file.
185
+ */
186
+ function summarizeDebugLog(filePath: string): {
187
+ events: number;
188
+ duration: string;
189
+ dispatches: number;
190
+ errors: Array<{ event: string; message: string }>;
191
+ } {
192
+ const result = {
193
+ events: 0,
194
+ duration: "unknown",
195
+ dispatches: 0,
196
+ errors: [] as Array<{ event: string; message: string }>,
197
+ };
198
+
199
+ let raw: string;
200
+ try { raw = readFileSync(filePath, "utf-8"); } catch { return result; }
201
+
202
+ const lines = raw.split("\n").filter(l => l.trim());
203
+ result.events = lines.length;
204
+
205
+ let firstTs = 0;
206
+ let lastTs = 0;
207
+
208
+ for (const line of lines) {
209
+ let entry: Record<string, unknown>;
210
+ try { entry = JSON.parse(line); } catch { continue; }
211
+
212
+ const ts = entry.ts as string | undefined;
213
+ if (ts) {
214
+ const t = new Date(ts).getTime();
215
+ if (!firstTs) firstTs = t;
216
+ lastTs = t;
217
+ }
218
+
219
+ const event = entry.event as string | undefined;
220
+ if (!event) continue;
221
+
222
+ if (event === "debug-summary") {
223
+ result.dispatches = (entry.dispatches as number) ?? 0;
224
+ }
225
+
226
+ if (event.includes("error") || event.includes("failed")) {
227
+ const msg = (entry.error as string) ?? (entry.message as string) ?? JSON.stringify(entry).slice(0, 100);
228
+ result.errors.push({ event, message: msg });
229
+ }
230
+ }
231
+
232
+ if (firstTs && lastTs) {
233
+ const elapsed = lastTs - firstTs;
234
+ const mins = Math.floor(elapsed / 60_000);
235
+ if (mins < 1) result.duration = `${Math.floor(elapsed / 1000)}s`;
236
+ else if (mins < 60) result.duration = `${mins}m`;
237
+ else result.duration = `${Math.floor(mins / 60)}h ${mins % 60}m`;
238
+ }
239
+
240
+ return result;
241
+ }
242
+
243
+ // ─── Main Handler ───────────────────────────────────────────────────────────
244
+
245
+ export async function handleLogs(args: string, ctx: ExtensionCommandContext): Promise<void> {
246
+ const basePath = process.cwd();
247
+ const parts = args.trim().split(/\s+/).filter(Boolean);
248
+ const subCmd = parts[0] ?? "";
249
+
250
+ // /gsd logs clear
251
+ if (subCmd === "clear") {
252
+ await handleLogsClear(basePath, ctx);
253
+ return;
254
+ }
255
+
256
+ // /gsd logs debug [N]
257
+ if (subCmd === "debug") {
258
+ const idx = parts[1] ? parseInt(parts[1], 10) : undefined;
259
+ await handleLogsDebug(basePath, ctx, idx);
260
+ return;
261
+ }
262
+
263
+ // /gsd logs tail [N]
264
+ if (subCmd === "tail") {
265
+ const count = parts[1] ? parseInt(parts[1], 10) : 5;
266
+ await handleLogsTail(basePath, ctx, count);
267
+ return;
268
+ }
269
+
270
+ // /gsd logs <N> — show specific activity log
271
+ if (subCmd && /^\d+$/.test(subCmd)) {
272
+ const seq = parseInt(subCmd, 10);
273
+ await handleLogsShow(basePath, ctx, seq);
274
+ return;
275
+ }
276
+
277
+ // /gsd logs — list overview
278
+ await handleLogsList(basePath, ctx);
279
+ }
280
+
281
+ // ─── Subcommand Handlers ────────────────────────────────────────────────────
282
+
283
+ async function handleLogsList(basePath: string, ctx: ExtensionCommandContext): Promise<void> {
284
+ const activities = listActivityLogs(basePath);
285
+ const debugLogs = listDebugLogs(basePath);
286
+
287
+ if (activities.length === 0 && debugLogs.length === 0) {
288
+ ctx.ui.notify(
289
+ "No logs found.\n\nActivity logs are created during auto-mode.\nDebug logs require GSD_DEBUG=1.",
290
+ "info",
291
+ );
292
+ return;
293
+ }
294
+
295
+ const lines: string[] = [];
296
+
297
+ if (activities.length > 0) {
298
+ lines.push("Activity Logs (.gsd/activity/):");
299
+ lines.push(" # Unit Type Unit ID Size Age");
300
+ lines.push(" " + "─".repeat(70));
301
+
302
+ // Show last 15 entries
303
+ const recent = activities.slice(-15);
304
+ for (const e of recent) {
305
+ const seq = String(e.seq).padStart(3, " ");
306
+ const type = e.unitType.padEnd(18, " ");
307
+ const id = e.unitId.padEnd(20, " ");
308
+ const size = formatSize(e.size).padStart(7, " ");
309
+ const age = formatAge(e.mtime);
310
+ lines.push(` ${seq} ${type} ${id} ${size} ${age}`);
311
+ }
312
+
313
+ if (activities.length > 15) {
314
+ lines.push(` ... and ${activities.length - 15} older entries`);
315
+ }
316
+ lines.push("");
317
+ lines.push(" View details: /gsd logs <#>");
318
+ }
319
+
320
+ if (debugLogs.length > 0) {
321
+ lines.push("");
322
+ lines.push("Debug Logs (.gsd/debug/):");
323
+ for (let i = 0; i < debugLogs.length; i++) {
324
+ const d = debugLogs[i];
325
+ const size = formatSize(d.size).padStart(7, " ");
326
+ const age = formatAge(d.mtime);
327
+ lines.push(` ${i + 1}. ${d.filename} ${size} ${age}`);
328
+ }
329
+ lines.push("");
330
+ lines.push(" View details: /gsd logs debug <#>");
331
+ }
332
+
333
+ // Metrics summary
334
+ const metricsPath = join(gsdRoot(basePath), "metrics.json");
335
+ const isMetrics = (d: unknown): d is { units: Array<Record<string, unknown>> } =>
336
+ d !== null && typeof d === "object" && "units" in d! && Array.isArray((d as Record<string, unknown>).units);
337
+ const metrics = loadJsonFileOrNull(metricsPath, isMetrics);
338
+ if (metrics && metrics.units.length > 0) {
339
+ const units = metrics.units;
340
+ const totalCost = units.reduce((sum: number, u) => sum + ((u.cost as number) ?? 0), 0);
341
+ const totalTokens = units.reduce((sum: number, u) => {
342
+ const t = u.tokens as Record<string, number> | undefined;
343
+ return sum + (t?.total ?? 0);
344
+ }, 0);
345
+ lines.push("");
346
+ lines.push(`Metrics: ${units.length} units tracked · $${totalCost.toFixed(2)} · ${(totalTokens / 1000).toFixed(0)}K tokens`);
347
+ }
348
+
349
+ lines.push("");
350
+ lines.push("Tip: Enable debug logging with GSD_DEBUG=1 before /gsd auto");
351
+
352
+ ctx.ui.notify(lines.join("\n"), "info");
353
+ }
354
+
355
+ async function handleLogsShow(basePath: string, ctx: ExtensionCommandContext, seq: number): Promise<void> {
356
+ const activities = listActivityLogs(basePath);
357
+ const entry = activities.find(e => e.seq === seq);
358
+
359
+ if (!entry) {
360
+ ctx.ui.notify(`Activity log #${seq} not found. Run /gsd logs to see available logs.`, "warning");
361
+ return;
362
+ }
363
+
364
+ const filePath = join(activityDir(basePath), entry.filename);
365
+ const summary = summarizeActivityLog(filePath);
366
+
367
+ const lines: string[] = [];
368
+ lines.push(`Activity Log #${entry.seq}: ${entry.unitType} — ${entry.unitId}`);
369
+ lines.push("─".repeat(60));
370
+ lines.push(`File: ${entry.filename}`);
371
+ lines.push(`Size: ${formatSize(entry.size)} | Age: ${formatAge(entry.mtime)}`);
372
+ lines.push(`Entries: ${summary.entryCount} | Tool calls: ${summary.toolCalls} | Errors: ${summary.errors}`);
373
+
374
+ if (summary.filesWritten.length > 0) {
375
+ lines.push("");
376
+ lines.push("Files written/edited:");
377
+ for (const f of summary.filesWritten.slice(0, 10)) {
378
+ lines.push(` ${f}`);
379
+ }
380
+ if (summary.filesWritten.length > 10) {
381
+ lines.push(` ... and ${summary.filesWritten.length - 10} more`);
382
+ }
383
+ }
384
+
385
+ if (summary.commandsRun.length > 0) {
386
+ lines.push("");
387
+ lines.push("Commands run:");
388
+ for (const c of summary.commandsRun.slice(0, 10)) {
389
+ const status = c.failed ? " FAILED" : "";
390
+ lines.push(` ${c.command}${status}`);
391
+ }
392
+ if (summary.commandsRun.length > 10) {
393
+ lines.push(` ... and ${summary.commandsRun.length - 10} more`);
394
+ }
395
+ }
396
+
397
+ if (summary.errors > 0) {
398
+ lines.push("");
399
+ lines.push(`${summary.errors} error(s) encountered during this unit.`);
400
+ }
401
+
402
+ if (summary.lastReasoning) {
403
+ lines.push("");
404
+ lines.push("Last reasoning:");
405
+ lines.push(` "${summary.lastReasoning}${summary.lastReasoning.length >= 200 ? "..." : ""}"`);
406
+ }
407
+
408
+ lines.push("");
409
+ lines.push(`Full log: ${filePath}`);
410
+
411
+ ctx.ui.notify(lines.join("\n"), "info");
412
+ }
413
+
414
+ async function handleLogsDebug(basePath: string, ctx: ExtensionCommandContext, idx?: number): Promise<void> {
415
+ const debugLogs = listDebugLogs(basePath);
416
+
417
+ if (debugLogs.length === 0) {
418
+ ctx.ui.notify(
419
+ "No debug logs found.\n\nEnable debug logging: GSD_DEBUG=1 gsd auto",
420
+ "info",
421
+ );
422
+ return;
423
+ }
424
+
425
+ if (idx === undefined) {
426
+ // List debug logs
427
+ const lines: string[] = ["Debug Logs (.gsd/debug/):", ""];
428
+ for (let i = 0; i < debugLogs.length; i++) {
429
+ const d = debugLogs[i];
430
+ lines.push(` ${i + 1}. ${d.filename} ${formatSize(d.size)} ${formatAge(d.mtime)}`);
431
+ }
432
+ lines.push("");
433
+ lines.push("View details: /gsd logs debug <#>");
434
+ ctx.ui.notify(lines.join("\n"), "info");
435
+ return;
436
+ }
437
+
438
+ // Show specific debug log
439
+ if (idx < 1 || idx > debugLogs.length) {
440
+ ctx.ui.notify(`Debug log #${idx} not found. Available: 1-${debugLogs.length}`, "warning");
441
+ return;
442
+ }
443
+
444
+ const entry = debugLogs[idx - 1];
445
+ const filePath = join(debugDir(basePath), entry.filename);
446
+ const summary = summarizeDebugLog(filePath);
447
+
448
+ const lines: string[] = [];
449
+ lines.push(`Debug Log: ${entry.filename}`);
450
+ lines.push("─".repeat(60));
451
+ lines.push(`Size: ${formatSize(entry.size)} | Age: ${formatAge(entry.mtime)}`);
452
+ lines.push(`Events: ${summary.events} | Duration: ${summary.duration} | Dispatches: ${summary.dispatches}`);
453
+
454
+ if (summary.errors.length > 0) {
455
+ lines.push("");
456
+ lines.push("Errors/failures:");
457
+ for (const e of summary.errors.slice(0, 10)) {
458
+ lines.push(` [${e.event}] ${e.message}`);
459
+ }
460
+ if (summary.errors.length > 10) {
461
+ lines.push(` ... and ${summary.errors.length - 10} more`);
462
+ }
463
+ }
464
+
465
+ lines.push("");
466
+ lines.push(`Full log: ${filePath}`);
467
+
468
+ ctx.ui.notify(lines.join("\n"), "info");
469
+ }
470
+
471
+ async function handleLogsTail(basePath: string, ctx: ExtensionCommandContext, count: number): Promise<void> {
472
+ const activities = listActivityLogs(basePath);
473
+
474
+ if (activities.length === 0) {
475
+ ctx.ui.notify("No activity logs found. Logs are created during auto-mode.", "info");
476
+ return;
477
+ }
478
+
479
+ const recent = activities.slice(-Math.max(1, Math.min(count, 20)));
480
+ const lines: string[] = [`Last ${recent.length} activity log(s):`, ""];
481
+
482
+ for (const e of recent) {
483
+ const filePath = join(activityDir(basePath), e.filename);
484
+ const summary = summarizeActivityLog(filePath);
485
+ const status = summary.errors > 0 ? `${summary.errors} err` : "ok";
486
+ lines.push(` #${e.seq} ${e.unitType} ${e.unitId} — ${summary.toolCalls} tools, ${status}, ${formatAge(e.mtime)}`);
487
+ }
488
+
489
+ ctx.ui.notify(lines.join("\n"), "info");
490
+ }
491
+
492
+ async function handleLogsClear(basePath: string, ctx: ExtensionCommandContext): Promise<void> {
493
+ let removedActivity = 0;
494
+ let removedDebug = 0;
495
+
496
+ // Clear activity logs older than 7 days, keep the 5 most recent
497
+ const activities = listActivityLogs(basePath);
498
+ const keepRecent = activities.slice(-5);
499
+ const keepSeqs = new Set(keepRecent.map(e => e.seq));
500
+ const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
501
+
502
+ for (const e of activities) {
503
+ if (keepSeqs.has(e.seq)) continue;
504
+ if (e.mtime.getTime() < cutoff) {
505
+ try {
506
+ unlinkSync(join(activityDir(basePath), e.filename));
507
+ removedActivity++;
508
+ } catch { /* ignore */ }
509
+ }
510
+ }
511
+
512
+ // Clear debug logs older than 3 days, keep latest 2
513
+ const debugLogs = listDebugLogs(basePath);
514
+ const keepDebug = debugLogs.slice(-2);
515
+ const keepDebugNames = new Set(keepDebug.map(d => d.filename));
516
+ const debugCutoff = Date.now() - 3 * 24 * 60 * 60 * 1000;
517
+
518
+ for (const d of debugLogs) {
519
+ if (keepDebugNames.has(d.filename)) continue;
520
+ if (d.mtime.getTime() < debugCutoff) {
521
+ try {
522
+ unlinkSync(join(debugDir(basePath), d.filename));
523
+ removedDebug++;
524
+ } catch { /* ignore */ }
525
+ }
526
+ }
527
+
528
+ if (removedActivity === 0 && removedDebug === 0) {
529
+ ctx.ui.notify("No old logs to clear.", "info");
530
+ } else {
531
+ ctx.ui.notify(
532
+ `Cleared ${removedActivity} activity log(s) and ${removedDebug} debug log(s).`,
533
+ "info",
534
+ );
535
+ }
536
+ }
@@ -22,6 +22,33 @@ import {
22
22
  import { loadFile, saveFile, splitFrontmatter, parseFrontmatterMap } from "./files.js";
23
23
  import { runClaudeImportFlow } from "./claude-import.js";
24
24
 
25
+ /** Extract body content after frontmatter closing delimiter, or null if none. */
26
+ function extractBodyAfterFrontmatter(content: string): string | null {
27
+ const closingIdx = content.indexOf("\n---", content.indexOf("---"));
28
+ if (closingIdx === -1) return null;
29
+ const afterFrontmatter = content.slice(closingIdx + 4);
30
+ return afterFrontmatter.trim() ? afterFrontmatter : null;
31
+ }
32
+
33
+ // ─── Numeric validation helpers ──────────────────────────────────────────────
34
+
35
+ /** Parse a string as a non-negative integer, or return null on failure. */
36
+ function tryParseInteger(val: string): number | null {
37
+ return /^\d+$/.test(val) ? Number(val) : null;
38
+ }
39
+
40
+ /** Parse a string as a finite number, or return null on failure. */
41
+ function tryParseNumber(val: string): number | null {
42
+ const n = Number(val);
43
+ return !isNaN(n) && isFinite(n) ? n : null;
44
+ }
45
+
46
+ /** Parse a string as a number in the 0–100 range, or return null on failure. */
47
+ function tryParsePercentage(val: string): number | null {
48
+ const n = Number(val);
49
+ return !isNaN(n) && n >= 0 && n <= 100 ? n : null;
50
+ }
51
+
25
52
  export async function handlePrefs(args: string, ctx: ExtensionCommandContext): Promise<void> {
26
53
  const trimmed = args.trim();
27
54
 
@@ -98,12 +125,8 @@ export async function handleImportClaude(ctx: ExtensionCommandContext, scope: "g
98
125
  const frontmatter = serializePreferencesToFrontmatter(prefs);
99
126
  let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
100
127
  if (existsSync(path)) {
101
- const existingContent = readFileSync(path, "utf-8");
102
- const closingIdx = existingContent.indexOf("\n---", existingContent.indexOf("---"));
103
- if (closingIdx !== -1) {
104
- const afterFrontmatter = existingContent.slice(closingIdx + 4);
105
- if (afterFrontmatter.trim()) body = afterFrontmatter;
106
- }
128
+ const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
129
+ if (preserved) body = preserved;
107
130
  }
108
131
  await saveFile(path, `---\n${frontmatter}---${body}`);
109
132
  };
@@ -124,14 +147,8 @@ export async function handlePrefsMode(ctx: ExtensionCommandContext, scope: "glob
124
147
 
125
148
  let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
126
149
  if (existsSync(path)) {
127
- const existingContent = readFileSync(path, "utf-8");
128
- const closingIdx = existingContent.indexOf("\n---", existingContent.indexOf("---"));
129
- if (closingIdx !== -1) {
130
- const afterFrontmatter = existingContent.slice(closingIdx + 4);
131
- if (afterFrontmatter.trim()) {
132
- body = afterFrontmatter;
133
- }
134
- }
150
+ const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
151
+ if (preserved) body = preserved;
135
152
  }
136
153
 
137
154
  const content = `---\n${frontmatter}---${body}`;
@@ -306,9 +323,10 @@ async function configureTimeouts(ctx: ExtensionCommandContext, prefs: Record<str
306
323
  );
307
324
  if (input !== null && input !== undefined) {
308
325
  const val = input.trim();
309
- if (val && /^\d+$/.test(val)) {
310
- autoSup[field.key] = Number(val);
311
- } else if (val && !/^\d+$/.test(val)) {
326
+ const parsed = tryParseInteger(val);
327
+ if (val && parsed !== null) {
328
+ autoSup[field.key] = parsed;
329
+ } else if (val) {
312
330
  ctx.ui.notify(`Invalid value "${val}" for ${field.label} — must be a whole number. Keeping previous value.`, "warning");
313
331
  } else if (!val && currentStr) {
314
332
  delete autoSup[field.key];
@@ -467,9 +485,10 @@ async function configureBudget(ctx: ExtensionCommandContext, prefs: Record<strin
467
485
  );
468
486
  if (ceilingInput !== null && ceilingInput !== undefined) {
469
487
  const val = ceilingInput.trim().replace(/^\$/, "");
470
- if (val && !isNaN(Number(val)) && isFinite(Number(val))) {
471
- prefs.budget_ceiling = Number(val);
472
- } else if (val && (isNaN(Number(val)) || !isFinite(Number(val)))) {
488
+ const parsed = tryParseNumber(val);
489
+ if (val && parsed !== null) {
490
+ prefs.budget_ceiling = parsed;
491
+ } else if (val) {
473
492
  ctx.ui.notify(`Invalid budget ceiling "${val}" — must be a number. Keeping previous value.`, "warning");
474
493
  } else if (!val && ceilingStr) {
475
494
  delete prefs.budget_ceiling;
@@ -493,14 +512,14 @@ async function configureBudget(ctx: ExtensionCommandContext, prefs: Record<strin
493
512
  );
494
513
  if (contextPauseInput !== null && contextPauseInput !== undefined) {
495
514
  const val = contextPauseInput.trim().replace(/%$/, "");
496
- if (val && !isNaN(Number(val)) && Number(val) >= 0 && Number(val) <= 100) {
497
- const num = Number(val);
498
- if (num === 0) {
515
+ const parsed = tryParsePercentage(val);
516
+ if (val && parsed !== null) {
517
+ if (parsed === 0) {
499
518
  delete prefs.context_pause_threshold;
500
519
  } else {
501
- prefs.context_pause_threshold = num;
520
+ prefs.context_pause_threshold = parsed;
502
521
  }
503
- } else if (val && (isNaN(Number(val)) || Number(val) < 0 || Number(val) > 100)) {
522
+ } else if (val) {
504
523
  ctx.ui.notify(`Invalid context pause threshold "${val}" — must be 0-100. Keeping previous value.`, "warning");
505
524
  }
506
525
  }
@@ -622,14 +641,8 @@ export async function handlePrefsWizard(
622
641
  // Preserve existing body content (everything after closing ---)
623
642
  let body = "\n# GSD Skill Preferences\n\nSee `~/.gsd/agent/extensions/gsd/docs/preferences-reference.md` for full field documentation and examples.\n";
624
643
  if (existsSync(path)) {
625
- const existingContent = readFileSync(path, "utf-8");
626
- const closingIdx = existingContent.indexOf("\n---", existingContent.indexOf("---"));
627
- if (closingIdx !== -1) {
628
- const afterFrontmatter = existingContent.slice(closingIdx + 4); // skip past "\n---"
629
- if (afterFrontmatter.trim()) {
630
- body = afterFrontmatter;
631
- }
632
- }
644
+ const preserved = extractBodyAfterFrontmatter(readFileSync(path, "utf-8"));
645
+ if (preserved) body = preserved;
633
646
  }
634
647
 
635
648
  const content = `---\n${frontmatter}---${body}`;