gsd-pi 2.28.0-dev.b23c118 → 2.28.0-dev.e19bf89
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/dist/resources/extensions/gsd/auto-post-unit.ts +3 -8
- package/dist/resources/extensions/gsd/auto-start.ts +9 -24
- package/dist/resources/extensions/gsd/auto.ts +2 -45
- package/dist/resources/extensions/gsd/commands.ts +0 -19
- package/dist/resources/extensions/gsd/doctor-types.ts +0 -13
- package/dist/resources/extensions/gsd/doctor.ts +6 -2
- package/dist/resources/extensions/gsd/export.ts +2 -28
- package/dist/resources/extensions/gsd/gsd-db.ts +0 -19
- package/package.json +3 -3
- package/src/resources/extensions/gsd/auto-post-unit.ts +3 -8
- package/src/resources/extensions/gsd/auto-start.ts +9 -24
- package/src/resources/extensions/gsd/auto.ts +2 -45
- package/src/resources/extensions/gsd/commands.ts +0 -19
- package/src/resources/extensions/gsd/doctor-types.ts +0 -13
- package/src/resources/extensions/gsd/doctor.ts +6 -2
- package/src/resources/extensions/gsd/export.ts +2 -28
- package/src/resources/extensions/gsd/gsd-db.ts +0 -19
- package/dist/resources/extensions/gsd/commands-logs.ts +0 -537
- package/dist/resources/extensions/gsd/session-lock.ts +0 -284
- package/dist/resources/extensions/gsd/tests/commands-logs.test.ts +0 -241
- package/dist/resources/extensions/gsd/tests/session-lock.test.ts +0 -315
- package/src/resources/extensions/gsd/commands-logs.ts +0 -537
- package/src/resources/extensions/gsd/session-lock.ts +0 -284
- package/src/resources/extensions/gsd/tests/commands-logs.test.ts +0 -241
- package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -315
|
@@ -32,19 +32,6 @@ export type DoctorIssueCode =
|
|
|
32
32
|
| "gitignore_missing_patterns"
|
|
33
33
|
| "unresolvable_dependency";
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
* Issue codes that represent expected completion-transition states.
|
|
37
|
-
* These are detected by the doctor but should NOT be auto-fixed at task level —
|
|
38
|
-
* they are resolved by the complete-slice/complete-milestone dispatch units.
|
|
39
|
-
* Consumers (e.g. auto-post-unit health tracking) should exclude these from
|
|
40
|
-
* error counts when running at task fixLevel to avoid false escalation.
|
|
41
|
-
*/
|
|
42
|
-
export const COMPLETION_TRANSITION_CODES = new Set<DoctorIssueCode>([
|
|
43
|
-
"all_tasks_done_missing_slice_summary",
|
|
44
|
-
"all_tasks_done_missing_slice_uat",
|
|
45
|
-
"all_tasks_done_roadmap_not_checked",
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
35
|
export interface DoctorIssue {
|
|
49
36
|
severity: DoctorSeverity;
|
|
50
37
|
code: DoctorIssueCode;
|
|
@@ -8,7 +8,6 @@ import { invalidateAllCaches } from "./cache.js";
|
|
|
8
8
|
import { loadEffectiveGSDPreferences, type GSDPreferences } from "./preferences.js";
|
|
9
9
|
|
|
10
10
|
import type { DoctorIssue, DoctorIssueCode } from "./doctor-types.js";
|
|
11
|
-
import { COMPLETION_TRANSITION_CODES } from "./doctor-types.js";
|
|
12
11
|
import { checkGitHealth, checkRuntimeHealth } from "./doctor-checks.js";
|
|
13
12
|
|
|
14
13
|
// ── Re-exports ─────────────────────────────────────────────────────────────
|
|
@@ -357,11 +356,16 @@ export async function runGSDDoctor(basePath: string, options?: { fix?: boolean;
|
|
|
357
356
|
// dispatch lifecycle (complete-slice, complete-milestone units), not to
|
|
358
357
|
// mechanical post-hook bookkeeping. When fixLevel is "task", these are
|
|
359
358
|
// detected and reported but never auto-fixed.
|
|
359
|
+
const completionTransitionCodes = new Set<DoctorIssueCode>([
|
|
360
|
+
"all_tasks_done_missing_slice_summary",
|
|
361
|
+
"all_tasks_done_missing_slice_uat",
|
|
362
|
+
"all_tasks_done_roadmap_not_checked",
|
|
363
|
+
]);
|
|
360
364
|
|
|
361
365
|
/** Whether a given issue code should be auto-fixed at the current fixLevel. */
|
|
362
366
|
const shouldFix = (code: DoctorIssueCode): boolean => {
|
|
363
367
|
if (!fix) return false;
|
|
364
|
-
if (fixLevel === "task" &&
|
|
368
|
+
if (fixLevel === "task" && completionTransitionCodes.has(code)) return false;
|
|
365
369
|
return true;
|
|
366
370
|
};
|
|
367
371
|
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
import type { ExtensionCommandContext } from "@gsd/pi-coding-agent";
|
|
5
5
|
import { writeFileSync, mkdirSync } from "node:fs";
|
|
6
6
|
import { join, basename } from "node:path";
|
|
7
|
-
import { exec } from "node:child_process";
|
|
8
7
|
import {
|
|
9
8
|
getLedger, getProjectTotals, aggregateByPhase, aggregateBySlice,
|
|
10
9
|
aggregateByModel, formatCost, formatTokenCount, loadLedgerFromDisk,
|
|
@@ -13,28 +12,6 @@ import type { UnitMetrics } from "./metrics.js";
|
|
|
13
12
|
import { gsdRoot } from "./paths.js";
|
|
14
13
|
import { formatDuration, fileLink } from "../shared/mod.js";
|
|
15
14
|
|
|
16
|
-
/**
|
|
17
|
-
* Open a file in the user's default browser.
|
|
18
|
-
* Uses platform-specific commands: `open` (macOS), `xdg-open` (Linux), `start` (Windows).
|
|
19
|
-
* Non-blocking, non-fatal — failures are silently ignored.
|
|
20
|
-
*/
|
|
21
|
-
export function openInBrowser(filePath: string): void {
|
|
22
|
-
const cmd =
|
|
23
|
-
process.platform === "darwin" ? "open" :
|
|
24
|
-
process.platform === "win32" ? "start" :
|
|
25
|
-
"xdg-open";
|
|
26
|
-
|
|
27
|
-
// On Windows, `start` needs an empty title argument when the path has spaces
|
|
28
|
-
const args = process.platform === "win32"
|
|
29
|
-
? `"" "${filePath}"`
|
|
30
|
-
: `"${filePath}"`;
|
|
31
|
-
|
|
32
|
-
exec(`${cmd} ${args}`, (err) => {
|
|
33
|
-
// Non-fatal — if the browser can't be opened, the file path is still shown
|
|
34
|
-
if (err) void err;
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
|
|
38
15
|
/**
|
|
39
16
|
* Write an export file directly, without requiring an ExtensionCommandContext.
|
|
40
17
|
* Used by the visualizer overlay export tab.
|
|
@@ -190,12 +167,10 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b
|
|
|
190
167
|
paths.push(bn(outPath));
|
|
191
168
|
}
|
|
192
169
|
|
|
193
|
-
const indexPath = join(gsdRoot(basePath), "reports", "index.html");
|
|
194
170
|
ctx.ui.notify(
|
|
195
|
-
`Generated ${paths.length} report snapshot${paths.length !== 1 ? "s" : ""}:\n${paths.map(p => ` ${p}`).join("\n")}\
|
|
171
|
+
`Generated ${paths.length} report snapshot${paths.length !== 1 ? "s" : ""}:\n${paths.map(p => ` ${p}`).join("\n")}\nBrowse all reports: .gsd/reports/index.html`,
|
|
196
172
|
"success",
|
|
197
173
|
);
|
|
198
|
-
openInBrowser(indexPath);
|
|
199
174
|
} else {
|
|
200
175
|
// Single report for the active milestone (existing behavior)
|
|
201
176
|
const doneSlices = data.milestones.reduce((s, m) => s + m.slices.filter(sl => sl.done).length, 0);
|
|
@@ -219,10 +194,9 @@ export async function handleExport(args: string, ctx: ExtensionCommandContext, b
|
|
|
219
194
|
phase: data.phase,
|
|
220
195
|
});
|
|
221
196
|
ctx.ui.notify(
|
|
222
|
-
`HTML report saved: .gsd/reports/${bn(outPath)}\
|
|
197
|
+
`HTML report saved: .gsd/reports/${bn(outPath)}\nBrowse all reports: .gsd/reports/index.html`,
|
|
223
198
|
"success",
|
|
224
199
|
);
|
|
225
|
-
openInBrowser(outPath);
|
|
226
200
|
}
|
|
227
201
|
} catch (err) {
|
|
228
202
|
ctx.ui.notify(
|
|
@@ -348,8 +348,6 @@ function migrateSchema(db: DbAdapter): void {
|
|
|
348
348
|
|
|
349
349
|
let currentDb: DbAdapter | null = null;
|
|
350
350
|
let currentPath: string | null = null;
|
|
351
|
-
/** PID that opened the current connection — used for diagnostic logging. */
|
|
352
|
-
let currentPid: number = 0;
|
|
353
351
|
|
|
354
352
|
// ─── Public API ────────────────────────────────────────────────────────────
|
|
355
353
|
|
|
@@ -397,7 +395,6 @@ export function openDatabase(path: string): boolean {
|
|
|
397
395
|
|
|
398
396
|
currentDb = adapter;
|
|
399
397
|
currentPath = path;
|
|
400
|
-
currentPid = process.pid;
|
|
401
398
|
return true;
|
|
402
399
|
}
|
|
403
400
|
|
|
@@ -413,7 +410,6 @@ export function closeDatabase(): void {
|
|
|
413
410
|
}
|
|
414
411
|
currentDb = null;
|
|
415
412
|
currentPath = null;
|
|
416
|
-
currentPid = 0;
|
|
417
413
|
}
|
|
418
414
|
}
|
|
419
415
|
|
|
@@ -728,21 +724,6 @@ export function reconcileWorktreeDb(
|
|
|
728
724
|
}
|
|
729
725
|
}
|
|
730
726
|
|
|
731
|
-
/**
|
|
732
|
-
* Returns the PID of the process that opened the current DB connection.
|
|
733
|
-
* Returns 0 if no connection is open.
|
|
734
|
-
*/
|
|
735
|
-
export function getDbOwnerPid(): number {
|
|
736
|
-
return currentPid;
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
/**
|
|
740
|
-
* Returns the path of the currently open database, or null if none.
|
|
741
|
-
*/
|
|
742
|
-
export function getDbPath(): string | null {
|
|
743
|
-
return currentPath;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
727
|
// ─── Internal Access (for testing) ─────────────────────────────────────────
|
|
747
728
|
|
|
748
729
|
/**
|
|
@@ -1,537 +0,0 @@
|
|
|
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
|
-
|
|
18
|
-
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
19
|
-
|
|
20
|
-
interface LogEntry {
|
|
21
|
-
seq: number;
|
|
22
|
-
filename: string;
|
|
23
|
-
unitType: string;
|
|
24
|
-
unitId: string;
|
|
25
|
-
size: number;
|
|
26
|
-
mtime: Date;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface DebugLogEntry {
|
|
30
|
-
filename: string;
|
|
31
|
-
size: number;
|
|
32
|
-
mtime: Date;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
function activityDir(basePath: string): string {
|
|
38
|
-
return join(gsdRoot(basePath), "activity");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function debugDir(basePath: string): string {
|
|
42
|
-
return join(gsdRoot(basePath), "debug");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function listActivityLogs(basePath: string): LogEntry[] {
|
|
46
|
-
const dir = activityDir(basePath);
|
|
47
|
-
if (!existsSync(dir)) return [];
|
|
48
|
-
|
|
49
|
-
const entries: LogEntry[] = [];
|
|
50
|
-
try {
|
|
51
|
-
for (const f of readdirSync(dir)) {
|
|
52
|
-
if (!f.endsWith(".jsonl")) continue;
|
|
53
|
-
// Filename format: {seq}-{unitType}-{unitId}.jsonl
|
|
54
|
-
// unitType is lowercase-with-hyphens (e.g., "execute-task", "complete-slice")
|
|
55
|
-
// unitId starts with M followed by digits (e.g., "M001-S01-T01")
|
|
56
|
-
const match = f.match(/^(\d+)-([\w-]+?)-(M\d[\w-]*)\.jsonl$/);
|
|
57
|
-
if (!match) continue;
|
|
58
|
-
|
|
59
|
-
const filePath = join(dir, f);
|
|
60
|
-
let stat;
|
|
61
|
-
try { stat = statSync(filePath); } catch { continue; }
|
|
62
|
-
|
|
63
|
-
entries.push({
|
|
64
|
-
seq: parseInt(match[1], 10),
|
|
65
|
-
filename: f,
|
|
66
|
-
unitType: match[2],
|
|
67
|
-
unitId: match[3].replace(/-/g, "/"),
|
|
68
|
-
size: stat.size,
|
|
69
|
-
mtime: stat.mtime,
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
} catch { /* dir not readable */ }
|
|
73
|
-
|
|
74
|
-
return entries.sort((a, b) => a.seq - b.seq);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function listDebugLogs(basePath: string): DebugLogEntry[] {
|
|
78
|
-
const dir = debugDir(basePath);
|
|
79
|
-
if (!existsSync(dir)) return [];
|
|
80
|
-
|
|
81
|
-
const entries: DebugLogEntry[] = [];
|
|
82
|
-
try {
|
|
83
|
-
for (const f of readdirSync(dir)) {
|
|
84
|
-
if (!f.endsWith(".log")) continue;
|
|
85
|
-
const filePath = join(dir, f);
|
|
86
|
-
let stat;
|
|
87
|
-
try { stat = statSync(filePath); } catch { continue; }
|
|
88
|
-
entries.push({ filename: f, size: stat.size, mtime: stat.mtime });
|
|
89
|
-
}
|
|
90
|
-
} catch { /* dir not readable */ }
|
|
91
|
-
|
|
92
|
-
return entries.sort((a, b) => a.mtime.getTime() - b.mtime.getTime());
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function formatSize(bytes: number): string {
|
|
96
|
-
if (bytes < 1024) return `${bytes}B`;
|
|
97
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
98
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function formatAge(date: Date): string {
|
|
102
|
-
const ms = Date.now() - date.getTime();
|
|
103
|
-
const mins = Math.floor(ms / 60_000);
|
|
104
|
-
if (mins < 1) return "just now";
|
|
105
|
-
if (mins < 60) return `${mins}m ago`;
|
|
106
|
-
const hrs = Math.floor(mins / 60);
|
|
107
|
-
if (hrs < 24) return `${hrs}h ago`;
|
|
108
|
-
const days = Math.floor(hrs / 24);
|
|
109
|
-
return `${days}d ago`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Extract a summary from an activity log JSONL file.
|
|
114
|
-
* Parses the entries to count tool calls, errors, and extract key events.
|
|
115
|
-
*/
|
|
116
|
-
function summarizeActivityLog(filePath: string): {
|
|
117
|
-
toolCalls: number;
|
|
118
|
-
errors: number;
|
|
119
|
-
filesWritten: string[];
|
|
120
|
-
commandsRun: Array<{ command: string; failed: boolean }>;
|
|
121
|
-
lastReasoning: string;
|
|
122
|
-
entryCount: number;
|
|
123
|
-
} {
|
|
124
|
-
const result = {
|
|
125
|
-
toolCalls: 0,
|
|
126
|
-
errors: 0,
|
|
127
|
-
filesWritten: new Set<string>(),
|
|
128
|
-
commandsRun: [] as Array<{ command: string; failed: boolean }>,
|
|
129
|
-
lastReasoning: "",
|
|
130
|
-
entryCount: 0,
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
let raw: string;
|
|
134
|
-
try { raw = readFileSync(filePath, "utf-8"); } catch { return { ...result, filesWritten: [] }; }
|
|
135
|
-
|
|
136
|
-
const lines = raw.split("\n").filter(l => l.trim());
|
|
137
|
-
result.entryCount = lines.length;
|
|
138
|
-
|
|
139
|
-
for (const line of lines) {
|
|
140
|
-
let entry: Record<string, unknown>;
|
|
141
|
-
try { entry = JSON.parse(line); } catch { continue; }
|
|
142
|
-
|
|
143
|
-
// Count tool calls
|
|
144
|
-
if (entry.type === "toolCall" || (entry.role === "assistant" && entry.content && Array.isArray(entry.content))) {
|
|
145
|
-
if (entry.type === "toolCall") {
|
|
146
|
-
result.toolCalls++;
|
|
147
|
-
const name = entry.name as string | undefined;
|
|
148
|
-
const args = entry.arguments as Record<string, unknown> | undefined;
|
|
149
|
-
|
|
150
|
-
if (name === "write" || name === "edit") {
|
|
151
|
-
const path = args?.file_path as string | undefined;
|
|
152
|
-
if (path) result.filesWritten.add(path);
|
|
153
|
-
}
|
|
154
|
-
if (name === "bash") {
|
|
155
|
-
const cmd = args?.command as string | undefined;
|
|
156
|
-
if (cmd) result.commandsRun.push({ command: cmd.slice(0, 80), failed: false });
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Count errors
|
|
162
|
-
if (entry.role === "toolResult" && entry.isError) {
|
|
163
|
-
result.errors++;
|
|
164
|
-
// Mark last command as failed
|
|
165
|
-
if (result.commandsRun.length > 0) {
|
|
166
|
-
result.commandsRun[result.commandsRun.length - 1].failed = true;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Track assistant reasoning
|
|
171
|
-
if (entry.role === "assistant" && typeof entry.content === "string") {
|
|
172
|
-
result.lastReasoning = entry.content.slice(0, 200);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
return {
|
|
177
|
-
...result,
|
|
178
|
-
filesWritten: [...result.filesWritten],
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Extract summary events from a debug log file.
|
|
184
|
-
*/
|
|
185
|
-
function summarizeDebugLog(filePath: string): {
|
|
186
|
-
events: number;
|
|
187
|
-
duration: string;
|
|
188
|
-
dispatches: number;
|
|
189
|
-
errors: Array<{ event: string; message: string }>;
|
|
190
|
-
} {
|
|
191
|
-
const result = {
|
|
192
|
-
events: 0,
|
|
193
|
-
duration: "unknown",
|
|
194
|
-
dispatches: 0,
|
|
195
|
-
errors: [] as Array<{ event: string; message: string }>,
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
let raw: string;
|
|
199
|
-
try { raw = readFileSync(filePath, "utf-8"); } catch { return result; }
|
|
200
|
-
|
|
201
|
-
const lines = raw.split("\n").filter(l => l.trim());
|
|
202
|
-
result.events = lines.length;
|
|
203
|
-
|
|
204
|
-
let firstTs = 0;
|
|
205
|
-
let lastTs = 0;
|
|
206
|
-
|
|
207
|
-
for (const line of lines) {
|
|
208
|
-
let entry: Record<string, unknown>;
|
|
209
|
-
try { entry = JSON.parse(line); } catch { continue; }
|
|
210
|
-
|
|
211
|
-
const ts = entry.ts as string | undefined;
|
|
212
|
-
if (ts) {
|
|
213
|
-
const t = new Date(ts).getTime();
|
|
214
|
-
if (!firstTs) firstTs = t;
|
|
215
|
-
lastTs = t;
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const event = entry.event as string | undefined;
|
|
219
|
-
if (!event) continue;
|
|
220
|
-
|
|
221
|
-
if (event === "debug-summary") {
|
|
222
|
-
result.dispatches = (entry.dispatches as number) ?? 0;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (event.includes("error") || event.includes("failed")) {
|
|
226
|
-
const msg = (entry.error as string) ?? (entry.message as string) ?? JSON.stringify(entry).slice(0, 100);
|
|
227
|
-
result.errors.push({ event, message: msg });
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (firstTs && lastTs) {
|
|
232
|
-
const elapsed = lastTs - firstTs;
|
|
233
|
-
const mins = Math.floor(elapsed / 60_000);
|
|
234
|
-
if (mins < 1) result.duration = `${Math.floor(elapsed / 1000)}s`;
|
|
235
|
-
else if (mins < 60) result.duration = `${mins}m`;
|
|
236
|
-
else result.duration = `${Math.floor(mins / 60)}h ${mins % 60}m`;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
return result;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ─── Main Handler ───────────────────────────────────────────────────────────
|
|
243
|
-
|
|
244
|
-
export async function handleLogs(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
245
|
-
const basePath = process.cwd();
|
|
246
|
-
const parts = args.trim().split(/\s+/).filter(Boolean);
|
|
247
|
-
const subCmd = parts[0] ?? "";
|
|
248
|
-
|
|
249
|
-
// /gsd logs clear
|
|
250
|
-
if (subCmd === "clear") {
|
|
251
|
-
await handleLogsClear(basePath, ctx);
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
// /gsd logs debug [N]
|
|
256
|
-
if (subCmd === "debug") {
|
|
257
|
-
const idx = parts[1] ? parseInt(parts[1], 10) : undefined;
|
|
258
|
-
await handleLogsDebug(basePath, ctx, idx);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// /gsd logs tail [N]
|
|
263
|
-
if (subCmd === "tail") {
|
|
264
|
-
const count = parts[1] ? parseInt(parts[1], 10) : 5;
|
|
265
|
-
await handleLogsTail(basePath, ctx, count);
|
|
266
|
-
return;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// /gsd logs <N> — show specific activity log
|
|
270
|
-
if (subCmd && /^\d+$/.test(subCmd)) {
|
|
271
|
-
const seq = parseInt(subCmd, 10);
|
|
272
|
-
await handleLogsShow(basePath, ctx, seq);
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// /gsd logs — list overview
|
|
277
|
-
await handleLogsList(basePath, ctx);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// ─── Subcommand Handlers ────────────────────────────────────────────────────
|
|
281
|
-
|
|
282
|
-
async function handleLogsList(basePath: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
283
|
-
const activities = listActivityLogs(basePath);
|
|
284
|
-
const debugLogs = listDebugLogs(basePath);
|
|
285
|
-
|
|
286
|
-
if (activities.length === 0 && debugLogs.length === 0) {
|
|
287
|
-
ctx.ui.notify(
|
|
288
|
-
"No logs found.\n\nActivity logs are created during auto-mode.\nDebug logs require GSD_DEBUG=1.",
|
|
289
|
-
"info",
|
|
290
|
-
);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const lines: string[] = [];
|
|
295
|
-
|
|
296
|
-
if (activities.length > 0) {
|
|
297
|
-
lines.push("Activity Logs (.gsd/activity/):");
|
|
298
|
-
lines.push(" # Unit Type Unit ID Size Age");
|
|
299
|
-
lines.push(" " + "─".repeat(70));
|
|
300
|
-
|
|
301
|
-
// Show last 15 entries
|
|
302
|
-
const recent = activities.slice(-15);
|
|
303
|
-
for (const e of recent) {
|
|
304
|
-
const seq = String(e.seq).padStart(3, " ");
|
|
305
|
-
const type = e.unitType.padEnd(18, " ");
|
|
306
|
-
const id = e.unitId.padEnd(20, " ");
|
|
307
|
-
const size = formatSize(e.size).padStart(7, " ");
|
|
308
|
-
const age = formatAge(e.mtime);
|
|
309
|
-
lines.push(` ${seq} ${type} ${id} ${size} ${age}`);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (activities.length > 15) {
|
|
313
|
-
lines.push(` ... and ${activities.length - 15} older entries`);
|
|
314
|
-
}
|
|
315
|
-
lines.push("");
|
|
316
|
-
lines.push(" View details: /gsd logs <#>");
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
if (debugLogs.length > 0) {
|
|
320
|
-
lines.push("");
|
|
321
|
-
lines.push("Debug Logs (.gsd/debug/):");
|
|
322
|
-
for (let i = 0; i < debugLogs.length; i++) {
|
|
323
|
-
const d = debugLogs[i];
|
|
324
|
-
const size = formatSize(d.size).padStart(7, " ");
|
|
325
|
-
const age = formatAge(d.mtime);
|
|
326
|
-
lines.push(` ${i + 1}. ${d.filename} ${size} ${age}`);
|
|
327
|
-
}
|
|
328
|
-
lines.push("");
|
|
329
|
-
lines.push(" View details: /gsd logs debug <#>");
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Metrics summary
|
|
333
|
-
const metricsPath = join(gsdRoot(basePath), "metrics.json");
|
|
334
|
-
if (existsSync(metricsPath)) {
|
|
335
|
-
try {
|
|
336
|
-
const metrics = JSON.parse(readFileSync(metricsPath, "utf-8"));
|
|
337
|
-
const units = metrics?.units;
|
|
338
|
-
if (Array.isArray(units) && units.length > 0) {
|
|
339
|
-
const totalCost = units.reduce((sum: number, u: Record<string, unknown>) => sum + ((u.cost as number) ?? 0), 0);
|
|
340
|
-
const totalTokens = units.reduce((sum: number, u: Record<string, unknown>) => {
|
|
341
|
-
const t = u.tokens as Record<string, number> | undefined;
|
|
342
|
-
return sum + (t?.total ?? 0);
|
|
343
|
-
}, 0);
|
|
344
|
-
lines.push("");
|
|
345
|
-
lines.push(`Metrics: ${units.length} units tracked · $${totalCost.toFixed(2)} · ${(totalTokens / 1000).toFixed(0)}K tokens`);
|
|
346
|
-
}
|
|
347
|
-
} catch { /* ignore */ }
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
lines.push("");
|
|
351
|
-
lines.push("Tip: Enable debug logging with GSD_DEBUG=1 before /gsd auto");
|
|
352
|
-
|
|
353
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
async function handleLogsShow(basePath: string, ctx: ExtensionCommandContext, seq: number): Promise<void> {
|
|
357
|
-
const activities = listActivityLogs(basePath);
|
|
358
|
-
const entry = activities.find(e => e.seq === seq);
|
|
359
|
-
|
|
360
|
-
if (!entry) {
|
|
361
|
-
ctx.ui.notify(`Activity log #${seq} not found. Run /gsd logs to see available logs.`, "warning");
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
const filePath = join(activityDir(basePath), entry.filename);
|
|
366
|
-
const summary = summarizeActivityLog(filePath);
|
|
367
|
-
|
|
368
|
-
const lines: string[] = [];
|
|
369
|
-
lines.push(`Activity Log #${entry.seq}: ${entry.unitType} — ${entry.unitId}`);
|
|
370
|
-
lines.push("─".repeat(60));
|
|
371
|
-
lines.push(`File: ${entry.filename}`);
|
|
372
|
-
lines.push(`Size: ${formatSize(entry.size)} | Age: ${formatAge(entry.mtime)}`);
|
|
373
|
-
lines.push(`Entries: ${summary.entryCount} | Tool calls: ${summary.toolCalls} | Errors: ${summary.errors}`);
|
|
374
|
-
|
|
375
|
-
if (summary.filesWritten.length > 0) {
|
|
376
|
-
lines.push("");
|
|
377
|
-
lines.push("Files written/edited:");
|
|
378
|
-
for (const f of summary.filesWritten.slice(0, 10)) {
|
|
379
|
-
lines.push(` ${f}`);
|
|
380
|
-
}
|
|
381
|
-
if (summary.filesWritten.length > 10) {
|
|
382
|
-
lines.push(` ... and ${summary.filesWritten.length - 10} more`);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (summary.commandsRun.length > 0) {
|
|
387
|
-
lines.push("");
|
|
388
|
-
lines.push("Commands run:");
|
|
389
|
-
for (const c of summary.commandsRun.slice(0, 10)) {
|
|
390
|
-
const status = c.failed ? " FAILED" : "";
|
|
391
|
-
lines.push(` ${c.command}${status}`);
|
|
392
|
-
}
|
|
393
|
-
if (summary.commandsRun.length > 10) {
|
|
394
|
-
lines.push(` ... and ${summary.commandsRun.length - 10} more`);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (summary.errors > 0) {
|
|
399
|
-
lines.push("");
|
|
400
|
-
lines.push(`${summary.errors} error(s) encountered during this unit.`);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (summary.lastReasoning) {
|
|
404
|
-
lines.push("");
|
|
405
|
-
lines.push("Last reasoning:");
|
|
406
|
-
lines.push(` "${summary.lastReasoning}${summary.lastReasoning.length >= 200 ? "..." : ""}"`);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
lines.push("");
|
|
410
|
-
lines.push(`Full log: ${filePath}`);
|
|
411
|
-
|
|
412
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
async function handleLogsDebug(basePath: string, ctx: ExtensionCommandContext, idx?: number): Promise<void> {
|
|
416
|
-
const debugLogs = listDebugLogs(basePath);
|
|
417
|
-
|
|
418
|
-
if (debugLogs.length === 0) {
|
|
419
|
-
ctx.ui.notify(
|
|
420
|
-
"No debug logs found.\n\nEnable debug logging: GSD_DEBUG=1 gsd auto",
|
|
421
|
-
"info",
|
|
422
|
-
);
|
|
423
|
-
return;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (idx === undefined) {
|
|
427
|
-
// List debug logs
|
|
428
|
-
const lines: string[] = ["Debug Logs (.gsd/debug/):", ""];
|
|
429
|
-
for (let i = 0; i < debugLogs.length; i++) {
|
|
430
|
-
const d = debugLogs[i];
|
|
431
|
-
lines.push(` ${i + 1}. ${d.filename} ${formatSize(d.size)} ${formatAge(d.mtime)}`);
|
|
432
|
-
}
|
|
433
|
-
lines.push("");
|
|
434
|
-
lines.push("View details: /gsd logs debug <#>");
|
|
435
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
436
|
-
return;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// Show specific debug log
|
|
440
|
-
if (idx < 1 || idx > debugLogs.length) {
|
|
441
|
-
ctx.ui.notify(`Debug log #${idx} not found. Available: 1-${debugLogs.length}`, "warning");
|
|
442
|
-
return;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const entry = debugLogs[idx - 1];
|
|
446
|
-
const filePath = join(debugDir(basePath), entry.filename);
|
|
447
|
-
const summary = summarizeDebugLog(filePath);
|
|
448
|
-
|
|
449
|
-
const lines: string[] = [];
|
|
450
|
-
lines.push(`Debug Log: ${entry.filename}`);
|
|
451
|
-
lines.push("─".repeat(60));
|
|
452
|
-
lines.push(`Size: ${formatSize(entry.size)} | Age: ${formatAge(entry.mtime)}`);
|
|
453
|
-
lines.push(`Events: ${summary.events} | Duration: ${summary.duration} | Dispatches: ${summary.dispatches}`);
|
|
454
|
-
|
|
455
|
-
if (summary.errors.length > 0) {
|
|
456
|
-
lines.push("");
|
|
457
|
-
lines.push("Errors/failures:");
|
|
458
|
-
for (const e of summary.errors.slice(0, 10)) {
|
|
459
|
-
lines.push(` [${e.event}] ${e.message}`);
|
|
460
|
-
}
|
|
461
|
-
if (summary.errors.length > 10) {
|
|
462
|
-
lines.push(` ... and ${summary.errors.length - 10} more`);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
lines.push("");
|
|
467
|
-
lines.push(`Full log: ${filePath}`);
|
|
468
|
-
|
|
469
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
async function handleLogsTail(basePath: string, ctx: ExtensionCommandContext, count: number): Promise<void> {
|
|
473
|
-
const activities = listActivityLogs(basePath);
|
|
474
|
-
|
|
475
|
-
if (activities.length === 0) {
|
|
476
|
-
ctx.ui.notify("No activity logs found. Logs are created during auto-mode.", "info");
|
|
477
|
-
return;
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const recent = activities.slice(-Math.max(1, Math.min(count, 20)));
|
|
481
|
-
const lines: string[] = [`Last ${recent.length} activity log(s):`, ""];
|
|
482
|
-
|
|
483
|
-
for (const e of recent) {
|
|
484
|
-
const filePath = join(activityDir(basePath), e.filename);
|
|
485
|
-
const summary = summarizeActivityLog(filePath);
|
|
486
|
-
const status = summary.errors > 0 ? `${summary.errors} err` : "ok";
|
|
487
|
-
lines.push(` #${e.seq} ${e.unitType} ${e.unitId} — ${summary.toolCalls} tools, ${status}, ${formatAge(e.mtime)}`);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
async function handleLogsClear(basePath: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
494
|
-
let removedActivity = 0;
|
|
495
|
-
let removedDebug = 0;
|
|
496
|
-
|
|
497
|
-
// Clear activity logs older than 7 days, keep the 5 most recent
|
|
498
|
-
const activities = listActivityLogs(basePath);
|
|
499
|
-
const keepRecent = activities.slice(-5);
|
|
500
|
-
const keepSeqs = new Set(keepRecent.map(e => e.seq));
|
|
501
|
-
const cutoff = Date.now() - 7 * 24 * 60 * 60 * 1000;
|
|
502
|
-
|
|
503
|
-
for (const e of activities) {
|
|
504
|
-
if (keepSeqs.has(e.seq)) continue;
|
|
505
|
-
if (e.mtime.getTime() < cutoff) {
|
|
506
|
-
try {
|
|
507
|
-
unlinkSync(join(activityDir(basePath), e.filename));
|
|
508
|
-
removedActivity++;
|
|
509
|
-
} catch { /* ignore */ }
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
// Clear debug logs older than 3 days, keep latest 2
|
|
514
|
-
const debugLogs = listDebugLogs(basePath);
|
|
515
|
-
const keepDebug = debugLogs.slice(-2);
|
|
516
|
-
const keepDebugNames = new Set(keepDebug.map(d => d.filename));
|
|
517
|
-
const debugCutoff = Date.now() - 3 * 24 * 60 * 60 * 1000;
|
|
518
|
-
|
|
519
|
-
for (const d of debugLogs) {
|
|
520
|
-
if (keepDebugNames.has(d.filename)) continue;
|
|
521
|
-
if (d.mtime.getTime() < debugCutoff) {
|
|
522
|
-
try {
|
|
523
|
-
unlinkSync(join(debugDir(basePath), d.filename));
|
|
524
|
-
removedDebug++;
|
|
525
|
-
} catch { /* ignore */ }
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (removedActivity === 0 && removedDebug === 0) {
|
|
530
|
-
ctx.ui.notify("No old logs to clear.", "info");
|
|
531
|
-
} else {
|
|
532
|
-
ctx.ui.notify(
|
|
533
|
-
`Cleared ${removedActivity} activity log(s) and ${removedDebug} debug log(s).`,
|
|
534
|
-
"info",
|
|
535
|
-
);
|
|
536
|
-
}
|
|
537
|
-
}
|