gsd-pi 2.31.2-dev.c8d7e03 → 2.32.0

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 (39) hide show
  1. package/dist/resources/extensions/gsd/auto-start.ts +4 -2
  2. package/dist/resources/extensions/gsd/commands.ts +19 -0
  3. package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
  4. package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
  5. package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
  6. package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
  7. package/dist/resources/extensions/gsd/doctor.ts +6 -0
  8. package/dist/resources/extensions/gsd/health-widget.ts +167 -0
  9. package/dist/resources/extensions/gsd/index.ts +6 -0
  10. package/dist/resources/extensions/gsd/progress-score.ts +273 -0
  11. package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
  12. package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
  13. package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
  14. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
  15. package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
  16. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
  17. package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
  18. package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
  19. package/package.json +1 -1
  20. package/packages/pi-coding-agent/package.json +1 -1
  21. package/pkg/package.json +1 -1
  22. package/src/resources/extensions/gsd/auto-start.ts +4 -2
  23. package/src/resources/extensions/gsd/commands.ts +19 -0
  24. package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
  25. package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
  26. package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
  27. package/src/resources/extensions/gsd/doctor-types.ts +14 -1
  28. package/src/resources/extensions/gsd/doctor.ts +6 -0
  29. package/src/resources/extensions/gsd/health-widget.ts +167 -0
  30. package/src/resources/extensions/gsd/index.ts +6 -0
  31. package/src/resources/extensions/gsd/progress-score.ts +273 -0
  32. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
  33. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
  34. package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
  35. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
  36. package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
  37. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
  38. package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
  39. package/src/resources/extensions/gsd/visualizer-views.ts +54 -0
@@ -18,6 +18,9 @@ import {
18
18
  } from './metrics.js';
19
19
  import { loadAllCaptures, countPendingCaptures } from './captures.js';
20
20
  import { loadEffectiveGSDPreferences } from './preferences.js';
21
+ import { runProviderChecks, type ProviderCheckResult } from './doctor-providers.js';
22
+ import { generateSkillHealthReport } from './skill-health.js';
23
+ import { runEnvironmentChecks, type EnvironmentCheckResult } from './doctor-environment.js';
21
24
 
22
25
  import type { Phase } from './types.js';
23
26
  import type { CaptureEntry } from './captures.js';
@@ -142,6 +145,22 @@ export interface CapturesInfo {
142
145
  totalCount: number;
143
146
  }
144
147
 
148
+ export interface ProviderStatusSummary {
149
+ name: string;
150
+ label: string;
151
+ category: string;
152
+ ok: boolean;
153
+ required: boolean;
154
+ message: string;
155
+ }
156
+
157
+ export interface SkillSummaryInfo {
158
+ total: number;
159
+ warningCount: number;
160
+ criticalCount: number;
161
+ topIssue: string | null;
162
+ }
163
+
145
164
  export interface HealthInfo {
146
165
  budgetCeiling: number | undefined;
147
166
  tokenProfile: string;
@@ -152,6 +171,9 @@ export interface HealthInfo {
152
171
  toolCalls: number;
153
172
  assistantMessages: number;
154
173
  userMessages: number;
174
+ providers: ProviderStatusSummary[];
175
+ skillSummary: SkillSummaryInfo;
176
+ environmentIssues: import("./doctor-environment.js").EnvironmentCheckResult[];
155
177
  }
156
178
 
157
179
  export interface VisualizerData {
@@ -538,7 +560,7 @@ function loadKnowledge(basePath: string): KnowledgeInfo {
538
560
 
539
561
  // ─── Health Loader ────────────────────────────────────────────────────────────
540
562
 
541
- function loadHealth(units: UnitMetrics[], totals: ProjectTotals | null): HealthInfo {
563
+ function loadHealth(units: UnitMetrics[], totals: ProjectTotals | null, basePath: string): HealthInfo {
542
564
  const prefs = loadEffectiveGSDPreferences();
543
565
  const budgetCeiling = prefs?.preferences?.budget_ceiling;
544
566
  const tokenProfile = prefs?.preferences?.token_profile ?? 'standard';
@@ -553,6 +575,39 @@ function loadHealth(units: UnitMetrics[], totals: ProjectTotals | null): HealthI
553
575
  const tierBreakdown = aggregateByTier(units);
554
576
  const tierSavingsLine = formatTierSavings(units);
555
577
 
578
+ // Provider checks — fast (auth.json + env vars only, no network)
579
+ let providers: ProviderStatusSummary[] = [];
580
+ try {
581
+ providers = runProviderChecks().map((r: ProviderCheckResult) => ({
582
+ name: r.name,
583
+ label: r.label,
584
+ category: r.category,
585
+ ok: r.status === "ok" || r.status === "unconfigured",
586
+ required: r.required,
587
+ message: r.message,
588
+ }));
589
+ } catch { /* non-fatal */ }
590
+
591
+ // Skill health summary
592
+ let skillSummary: SkillSummaryInfo = { total: 0, warningCount: 0, criticalCount: 0, topIssue: null };
593
+ try {
594
+ const report = generateSkillHealthReport(basePath);
595
+ const warnings = report.suggestions.filter(s => s.severity === "warning");
596
+ const criticals = report.suggestions.filter(s => s.severity === "critical");
597
+ skillSummary = {
598
+ total: report.skills.length,
599
+ warningCount: warnings.length,
600
+ criticalCount: criticals.length,
601
+ topIssue: report.suggestions[0]?.message ?? null,
602
+ };
603
+ } catch { /* non-fatal */ }
604
+
605
+ // Environment issues (from doctor-environment.ts, #1221)
606
+ let environmentIssues: EnvironmentCheckResult[] = [];
607
+ try {
608
+ environmentIssues = runEnvironmentChecks(basePath).filter(r => r.status !== "ok");
609
+ } catch { /* non-fatal */ }
610
+
556
611
  return {
557
612
  budgetCeiling,
558
613
  tokenProfile,
@@ -563,6 +618,9 @@ function loadHealth(units: UnitMetrics[], totals: ProjectTotals | null): HealthI
563
618
  toolCalls: totals?.toolCalls ?? 0,
564
619
  assistantMessages: totals?.assistantMessages ?? 0,
565
620
  userMessages: totals?.userMessages ?? 0,
621
+ providers,
622
+ skillSummary,
623
+ environmentIssues,
566
624
  };
567
625
  }
568
626
 
@@ -780,7 +838,7 @@ export async function loadVisualizerData(basePath: string): Promise<VisualizerDa
780
838
  totalCount: allCaptures.length,
781
839
  };
782
840
 
783
- const health = loadHealth(units, totals);
841
+ const health = loadHealth(units, totals, basePath);
784
842
  const stats = buildVisualizerStats(milestones, changelog.entries);
785
843
  const discussion = loadDiscussionState(basePath, milestones);
786
844
 
@@ -1113,5 +1113,59 @@ export function renderHealthView(
1113
1113
  lines.push(` Tool calls: ${th.fg("text", String(health.toolCalls))}`);
1114
1114
  lines.push(` Messages: ${th.fg("text", String(health.assistantMessages))} sent / ${th.fg("text", String(health.userMessages))} received`);
1115
1115
 
1116
+ // Environment section — issues only (from doctor-environment.ts, #1221)
1117
+ if (health.environmentIssues?.length > 0) {
1118
+ lines.push("");
1119
+ lines.push(th.fg("accent", th.bold("Environment")));
1120
+ lines.push("");
1121
+ for (const r of health.environmentIssues) {
1122
+ const icon = r.status === "error" ? th.fg("error", "✗") : th.fg("warning", "⚠");
1123
+ lines.push(` ${icon} ${th.fg("text", r.message)}`);
1124
+ if (r.detail) lines.push(` ${th.fg("dim", r.detail)}`);
1125
+ }
1126
+ }
1127
+
1128
+ // Providers section
1129
+ if (health.providers?.length > 0) {
1130
+ lines.push("");
1131
+ lines.push(th.fg("accent", th.bold("Providers")));
1132
+ lines.push("");
1133
+ const categoryOrder = ["llm", "remote", "search", "tool"];
1134
+ const categoryLabels: Record<string, string> = { llm: "LLM", remote: "Notifications", search: "Search", tool: "Tools" };
1135
+ const grouped = new Map<string, typeof health.providers>();
1136
+ for (const p of health.providers) {
1137
+ const cat = p.category;
1138
+ if (!grouped.has(cat)) grouped.set(cat, []);
1139
+ grouped.get(cat)!.push(p);
1140
+ }
1141
+ for (const cat of categoryOrder) {
1142
+ const items = grouped.get(cat);
1143
+ if (!items || items.length === 0) continue;
1144
+ lines.push(` ${th.fg("dim", categoryLabels[cat] ?? cat)}`);
1145
+ for (const p of items) {
1146
+ const icon = p.ok ? th.fg("success", "✓") : th.fg("error", "✗");
1147
+ const msg = p.ok ? th.fg("dim", p.message) : th.fg("text", p.message);
1148
+ lines.push(` ${icon} ${msg}`);
1149
+ }
1150
+ }
1151
+ }
1152
+
1153
+ // Skills section
1154
+ if (health.skillSummary?.total > 0) {
1155
+ lines.push("");
1156
+ lines.push(th.fg("accent", th.bold("Skills")));
1157
+ lines.push("");
1158
+ const { total, warningCount, criticalCount, topIssue } = health.skillSummary;
1159
+ const issueColor = criticalCount > 0 ? "error" : warningCount > 0 ? "warning" : "success";
1160
+ const issueTag = criticalCount > 0
1161
+ ? `${criticalCount} critical`
1162
+ : warningCount > 0
1163
+ ? `${warningCount} warning${warningCount > 1 ? "s" : ""}`
1164
+ : "all healthy";
1165
+ lines.push(` ${th.fg("text", String(total))} skills tracked · ${th.fg(issueColor, issueTag)}`);
1166
+ if (topIssue) lines.push(` ${th.fg("warning", "⚠")} ${th.fg("dim", topIssue)}`);
1167
+ lines.push(` ${th.fg("dim", "→ /gsd skill-health for full report")}`);
1168
+ }
1169
+
1116
1170
  return lines;
1117
1171
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.31.2-dev.c8d7e03",
3
+ "version": "2.32.0",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gsd/pi-coding-agent",
3
- "version": "2.31.2",
3
+ "version": "2.32.0",
4
4
  "description": "Coding agent CLI (vendored from pi-mono)",
5
5
  "type": "module",
6
6
  "piConfig": {
package/pkg/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glittercowboy/gsd",
3
- "version": "2.31.2",
3
+ "version": "2.32.0",
4
4
  "piConfig": {
5
5
  "name": "gsd",
6
6
  "configDir": ".gsd"
@@ -131,9 +131,11 @@ export async function bootstrapAutoSession(
131
131
  // Initialize GitServiceImpl
132
132
  s.gitService = createGitService(s.basePath);
133
133
 
134
- // Check for crash from previous session (use both old and new lock data)
134
+ // Check for crash from previous session (use both old and new lock data).
135
+ // Skip if the lock PID matches this process — acquireSessionLock() writes
136
+ // to the same auto.lock file before this check, so we'd always false-positive.
135
137
  const crashLock = readCrashLock(base);
136
- if (crashLock) {
138
+ if (crashLock && crashLock.pid !== process.pid) {
137
139
  // We already hold the session lock, so no concurrent session is running.
138
140
  // The crash lock is from a dead process — recover context from it.
139
141
  const recoveredMid = crashLock.unitId.split("/")[0];
@@ -44,6 +44,8 @@ import { handleConfig } from "./commands-config.js";
44
44
  import { handleInspect } from "./commands-inspect.js";
45
45
  import { handleCleanupBranches, handleCleanupSnapshots, handleSkip, handleDryRun } from "./commands-maintenance.js";
46
46
  import { handleDoctor, handleSteer, handleCapture, handleTriage, handleKnowledge, handleRunHook, handleUpdate, handleSkillHealth } from "./commands-handlers.js";
47
+ import { computeProgressScore, formatProgressLine } from "./progress-score.js";
48
+ import { runEnvironmentChecks } from "./doctor-environment.js";
47
49
  import { handleLogs } from "./commands-logs.js";
48
50
  import { handleStart, handleTemplates, getTemplateCompletions } from "./commands-workflow-templates.js";
49
51
 
@@ -1068,6 +1070,11 @@ async function handleSetup(args: string, ctx: ExtensionCommandContext): Promise<
1068
1070
  function formatTextStatus(state: GSDState): string {
1069
1071
  const lines: string[] = ["GSD Status\n"];
1070
1072
 
1073
+ // Progress score — traffic light (#1221)
1074
+ const progressScore = computeProgressScore();
1075
+ lines.push(formatProgressLine(progressScore));
1076
+ lines.push("");
1077
+
1071
1078
  // Phase
1072
1079
  lines.push(`Phase: ${state.phase}`);
1073
1080
 
@@ -1114,5 +1121,17 @@ function formatTextStatus(state: GSDState): string {
1114
1121
  }
1115
1122
  }
1116
1123
 
1124
+ // Environment health (#1221)
1125
+ const envResults = runEnvironmentChecks(projectRoot());
1126
+ const envIssues = envResults.filter(r => r.status !== "ok");
1127
+ if (envIssues.length > 0) {
1128
+ lines.push("");
1129
+ lines.push("Environment:");
1130
+ for (const r of envIssues) {
1131
+ const icon = r.status === "error" ? "✗" : "⚠";
1132
+ lines.push(` ${icon} ${r.message}`);
1133
+ }
1134
+ }
1135
+
1117
1136
  return lines.join("\n");
1118
1137
  }
@@ -23,6 +23,8 @@ import { getActiveWorktreeName } from "./worktree-command.js";
23
23
  import { getWorkerBatches, hasActiveWorkers, type WorkerEntry } from "../subagent/worker-registry.js";
24
24
  import { formatDuration, padRight, joinColumns, centerLine, fitColumns, STATUS_GLYPH, STATUS_COLOR } from "../shared/mod.js";
25
25
  import { estimateTimeRemaining } from "./auto-dashboard.js";
26
+ import { computeProgressScore, formatProgressLine } from "./progress-score.js";
27
+ import { runEnvironmentChecks, type EnvironmentCheckResult } from "./doctor-environment.js";
26
28
 
27
29
  function unitLabel(type: string): string {
28
30
  switch (type) {
@@ -310,6 +312,15 @@ export class GSDDashboardOverlay {
310
312
  elapsedParts = th.fg("dim", `since ${this.dashData.remoteSession!.startedAt.replace("T", " ").slice(0, 19)}`);
311
313
  }
312
314
  lines.push(row(joinColumns(`${title} ${status}${worktreeTag}`, elapsedParts, contentWidth)));
315
+
316
+ // Progress score — traffic light indicator (#1221)
317
+ if (this.dashData.active || this.dashData.paused) {
318
+ const progressScore = computeProgressScore();
319
+ const progressIcon = progressScore.level === "green" ? th.fg("success", "●")
320
+ : progressScore.level === "yellow" ? th.fg("warning", "●")
321
+ : th.fg("error", "●");
322
+ lines.push(row(`${progressIcon} ${th.fg("text", progressScore.summary)}`));
323
+ }
313
324
  lines.push(blank());
314
325
 
315
326
  if (this.dashData.currentUnit) {
@@ -579,6 +590,23 @@ export class GSDDashboardOverlay {
579
590
  }
580
591
  }
581
592
 
593
+ // Environment health section (#1221) — only show issues
594
+ const envResults = runEnvironmentChecks(this.dashData.basePath || process.cwd());
595
+ const envIssues = envResults.filter(r => r.status !== "ok");
596
+ if (envIssues.length > 0) {
597
+ lines.push(blank());
598
+ lines.push(hr());
599
+ lines.push(row(th.fg("text", th.bold("Environment"))));
600
+ lines.push(blank());
601
+ for (const r of envIssues) {
602
+ const icon = r.status === "error" ? th.fg("error", "✗") : th.fg("warning", "⚠");
603
+ lines.push(row(` ${icon} ${th.fg("text", r.message)}`));
604
+ if (r.detail) {
605
+ lines.push(row(th.fg("dim", ` ${r.detail}`)));
606
+ }
607
+ }
608
+ }
609
+
582
610
  lines.push(blank());
583
611
  lines.push(hr());
584
612
  lines.push(centered(th.fg("dim", "↑↓ scroll · g/G top/end · esc close")));