gsd-pi 2.31.2-dev.2453512 → 2.31.2-dev.392f611

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 (40) hide show
  1. package/dist/cli.js +5 -5
  2. package/dist/resources/extensions/gsd/auto-start.ts +4 -2
  3. package/dist/resources/extensions/gsd/commands.ts +19 -0
  4. package/dist/resources/extensions/gsd/dashboard-overlay.ts +28 -0
  5. package/dist/resources/extensions/gsd/doctor-environment.ts +497 -0
  6. package/dist/resources/extensions/gsd/doctor-providers.ts +343 -0
  7. package/dist/resources/extensions/gsd/doctor-types.ts +14 -1
  8. package/dist/resources/extensions/gsd/doctor.ts +6 -0
  9. package/dist/resources/extensions/gsd/health-widget.ts +167 -0
  10. package/dist/resources/extensions/gsd/index.ts +6 -0
  11. package/dist/resources/extensions/gsd/progress-score.ts +273 -0
  12. package/dist/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
  13. package/dist/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
  14. package/dist/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
  15. package/dist/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
  16. package/dist/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
  17. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
  18. package/dist/resources/extensions/gsd/visualizer-data.ts +60 -2
  19. package/dist/resources/extensions/gsd/visualizer-views.ts +54 -0
  20. package/dist/worktree-cli.d.ts +42 -6
  21. package/dist/worktree-cli.js +88 -48
  22. package/package.json +1 -1
  23. package/src/resources/extensions/gsd/auto-start.ts +4 -2
  24. package/src/resources/extensions/gsd/commands.ts +19 -0
  25. package/src/resources/extensions/gsd/dashboard-overlay.ts +28 -0
  26. package/src/resources/extensions/gsd/doctor-environment.ts +497 -0
  27. package/src/resources/extensions/gsd/doctor-providers.ts +343 -0
  28. package/src/resources/extensions/gsd/doctor-types.ts +14 -1
  29. package/src/resources/extensions/gsd/doctor.ts +6 -0
  30. package/src/resources/extensions/gsd/health-widget.ts +167 -0
  31. package/src/resources/extensions/gsd/index.ts +6 -0
  32. package/src/resources/extensions/gsd/progress-score.ts +273 -0
  33. package/src/resources/extensions/gsd/tests/doctor-environment.test.ts +314 -0
  34. package/src/resources/extensions/gsd/tests/doctor-providers.test.ts +298 -0
  35. package/src/resources/extensions/gsd/tests/export-html-enhancements.test.ts +3 -0
  36. package/src/resources/extensions/gsd/tests/memory-leak-guards.test.ts +7 -3
  37. package/src/resources/extensions/gsd/tests/progress-score.test.ts +206 -0
  38. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +12 -0
  39. package/src/resources/extensions/gsd/visualizer-data.ts +60 -2
  40. 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
  }
@@ -12,7 +12,43 @@
12
12
  * On session exit (via session_shutdown event), auto-commits dirty work
13
13
  * so nothing is lost. The GSD extension reads GSD_CLI_WORKTREE to know
14
14
  * when a session was launched via -w.
15
+ *
16
+ * Note: Extension modules are .ts files loaded via jiti (not compiled to .js).
17
+ * We use createJiti() here because this module is compiled by tsc but imports
18
+ * from resources/extensions/gsd/ which are shipped as raw .ts (#1283).
15
19
  */
20
+ interface ExtensionModules {
21
+ createWorktree: (basePath: string, name: string) => {
22
+ path: string;
23
+ branch: string;
24
+ };
25
+ listWorktrees: (basePath: string) => Array<{
26
+ name: string;
27
+ path: string;
28
+ branch: string;
29
+ }>;
30
+ removeWorktree: (basePath: string, name: string, opts?: {
31
+ deleteBranch?: boolean;
32
+ }) => void;
33
+ mergeWorktreeToMain: (basePath: string, name: string, commitMessage: string) => void;
34
+ diffWorktreeAll: (basePath: string, name: string) => {
35
+ added: any[];
36
+ modified: any[];
37
+ removed: any[];
38
+ };
39
+ diffWorktreeNumstat: (basePath: string, name: string) => Array<{
40
+ added: number;
41
+ removed: number;
42
+ }>;
43
+ worktreeBranchName: (name: string) => string;
44
+ worktreePath: (basePath: string, name: string) => string;
45
+ runWorktreePostCreateHook: (basePath: string, wtPath: string) => string | null;
46
+ nativeHasChanges: (path: string) => boolean;
47
+ nativeDetectMainBranch: (basePath: string) => string;
48
+ nativeCommitCountBetween: (basePath: string, from: string, to: string) => number;
49
+ inferCommitType: (name: string) => string;
50
+ autoCommitCurrentBranch: (wtPath: string, reason: string, name: string) => void;
51
+ }
16
52
  interface WorktreeStatus {
17
53
  name: string;
18
54
  path: string;
@@ -24,11 +60,11 @@ interface WorktreeStatus {
24
60
  uncommitted: boolean;
25
61
  commits: number;
26
62
  }
27
- declare function getWorktreeStatus(basePath: string, name: string, wtPath: string): WorktreeStatus;
28
- declare function handleList(basePath: string): void;
63
+ declare function getWorktreeStatus(ext: ExtensionModules, basePath: string, name: string, wtPath: string): WorktreeStatus;
64
+ declare function handleList(basePath: string): Promise<void>;
29
65
  declare function handleMerge(basePath: string, args: string[]): Promise<void>;
30
- declare function handleClean(basePath: string): void;
31
- declare function handleRemove(basePath: string, args: string[]): void;
32
- declare function handleStatusBanner(basePath: string): void;
33
- declare function handleWorktreeFlag(worktreeFlag: boolean | string): void;
66
+ declare function handleClean(basePath: string): Promise<void>;
67
+ declare function handleRemove(basePath: string, args: string[]): Promise<void>;
68
+ declare function handleStatusBanner(basePath: string): Promise<void>;
69
+ declare function handleWorktreeFlag(worktreeFlag: boolean | string): Promise<void>;
34
70
  export { handleList, handleMerge, handleClean, handleRemove, handleStatusBanner, handleWorktreeFlag, getWorktreeStatus, };
@@ -12,18 +12,53 @@
12
12
  * On session exit (via session_shutdown event), auto-commits dirty work
13
13
  * so nothing is lost. The GSD extension reads GSD_CLI_WORKTREE to know
14
14
  * when a session was launched via -w.
15
+ *
16
+ * Note: Extension modules are .ts files loaded via jiti (not compiled to .js).
17
+ * We use createJiti() here because this module is compiled by tsc but imports
18
+ * from resources/extensions/gsd/ which are shipped as raw .ts (#1283).
15
19
  */
16
20
  import chalk from 'chalk';
17
- import { createWorktree, listWorktrees, removeWorktree, mergeWorktreeToMain, diffWorktreeAll, diffWorktreeNumstat, worktreeBranchName, } from './resources/extensions/gsd/worktree-manager.js';
18
- import { runWorktreePostCreateHook } from './resources/extensions/gsd/auto-worktree.js';
21
+ import { createJiti } from '@mariozechner/jiti';
22
+ import { fileURLToPath } from 'node:url';
23
+ import { dirname, join } from 'node:path';
19
24
  import { generateWorktreeName } from './worktree-name-gen.js';
20
- import { nativeHasChanges, nativeDetectMainBranch, nativeCommitCountBetween, } from './resources/extensions/gsd/native-git-bridge.js';
21
- import { inferCommitType } from './resources/extensions/gsd/git-service.js';
22
25
  import { existsSync } from 'node:fs';
26
+ const __dirname = dirname(fileURLToPath(import.meta.url));
27
+ const jiti = createJiti(fileURLToPath(import.meta.url), { interopDefault: true, debug: false });
28
+ // Lazily-loaded extension modules (loaded once on first use via jiti)
29
+ let _ext = null;
30
+ async function loadExtensionModules() {
31
+ if (_ext)
32
+ return _ext;
33
+ const [wtMgr, autoWt, gitBridge, gitSvc, wt] = await Promise.all([
34
+ jiti.import(join(__dirname, 'resources/extensions/gsd/worktree-manager.ts'), {}),
35
+ jiti.import(join(__dirname, 'resources/extensions/gsd/auto-worktree.ts'), {}),
36
+ jiti.import(join(__dirname, 'resources/extensions/gsd/native-git-bridge.ts'), {}),
37
+ jiti.import(join(__dirname, 'resources/extensions/gsd/git-service.ts'), {}),
38
+ jiti.import(join(__dirname, 'resources/extensions/gsd/worktree.ts'), {}),
39
+ ]);
40
+ _ext = {
41
+ createWorktree: wtMgr.createWorktree,
42
+ listWorktrees: wtMgr.listWorktrees,
43
+ removeWorktree: wtMgr.removeWorktree,
44
+ mergeWorktreeToMain: wtMgr.mergeWorktreeToMain,
45
+ diffWorktreeAll: wtMgr.diffWorktreeAll,
46
+ diffWorktreeNumstat: wtMgr.diffWorktreeNumstat,
47
+ worktreeBranchName: wtMgr.worktreeBranchName,
48
+ worktreePath: wtMgr.worktreePath,
49
+ runWorktreePostCreateHook: autoWt.runWorktreePostCreateHook,
50
+ nativeHasChanges: gitBridge.nativeHasChanges,
51
+ nativeDetectMainBranch: gitBridge.nativeDetectMainBranch,
52
+ nativeCommitCountBetween: gitBridge.nativeCommitCountBetween,
53
+ inferCommitType: gitSvc.inferCommitType,
54
+ autoCommitCurrentBranch: wt.autoCommitCurrentBranch,
55
+ };
56
+ return _ext;
57
+ }
23
58
  // ─── Status Helpers ─────────────────────────────────────────────────────────
24
- function getWorktreeStatus(basePath, name, wtPath) {
25
- const diff = diffWorktreeAll(basePath, name);
26
- const numstat = diffWorktreeNumstat(basePath, name);
59
+ function getWorktreeStatus(ext, basePath, name, wtPath) {
60
+ const diff = ext.diffWorktreeAll(basePath, name);
61
+ const numstat = ext.diffWorktreeNumstat(basePath, name);
27
62
  const filesChanged = diff.added.length + diff.modified.length + diff.removed.length;
28
63
  let linesAdded = 0;
29
64
  let linesRemoved = 0;
@@ -33,19 +68,19 @@ function getWorktreeStatus(basePath, name, wtPath) {
33
68
  }
34
69
  let uncommitted = false;
35
70
  try {
36
- uncommitted = existsSync(wtPath) && nativeHasChanges(wtPath);
71
+ uncommitted = existsSync(wtPath) && ext.nativeHasChanges(wtPath);
37
72
  }
38
73
  catch { /* */ }
39
74
  let commits = 0;
40
75
  try {
41
- const mainBranch = nativeDetectMainBranch(basePath);
42
- commits = nativeCommitCountBetween(basePath, mainBranch, worktreeBranchName(name));
76
+ const mainBranch = ext.nativeDetectMainBranch(basePath);
77
+ commits = ext.nativeCommitCountBetween(basePath, mainBranch, ext.worktreeBranchName(name));
43
78
  }
44
79
  catch { /* */ }
45
80
  return {
46
81
  name,
47
82
  path: wtPath,
48
- branch: worktreeBranchName(name),
83
+ branch: ext.worktreeBranchName(name),
49
84
  exists: existsSync(wtPath),
50
85
  filesChanged,
51
86
  linesAdded,
@@ -71,65 +106,66 @@ function formatStatus(s) {
71
106
  return lines.join('\n');
72
107
  }
73
108
  // ─── Subcommand: list ───────────────────────────────────────────────────────
74
- function handleList(basePath) {
75
- const worktrees = listWorktrees(basePath);
109
+ async function handleList(basePath) {
110
+ const ext = await loadExtensionModules();
111
+ const worktrees = ext.listWorktrees(basePath);
76
112
  if (worktrees.length === 0) {
77
113
  process.stderr.write(chalk.dim('No worktrees. Create one with: gsd -w <name>\n'));
78
114
  return;
79
115
  }
80
116
  process.stderr.write(chalk.bold('\nWorktrees\n\n'));
81
117
  for (const wt of worktrees) {
82
- const status = getWorktreeStatus(basePath, wt.name, wt.path);
118
+ const status = getWorktreeStatus(ext, basePath, wt.name, wt.path);
83
119
  process.stderr.write(formatStatus(status) + '\n\n');
84
120
  }
85
121
  }
86
122
  // ─── Subcommand: merge ──────────────────────────────────────────────────────
87
123
  async function handleMerge(basePath, args) {
124
+ const ext = await loadExtensionModules();
88
125
  const name = args[0];
89
126
  if (!name) {
90
127
  // If only one worktree exists, merge it
91
- const worktrees = listWorktrees(basePath);
128
+ const worktrees = ext.listWorktrees(basePath);
92
129
  if (worktrees.length === 1) {
93
- await doMerge(basePath, worktrees[0].name);
130
+ await doMerge(ext, basePath, worktrees[0].name);
94
131
  return;
95
132
  }
96
133
  process.stderr.write(chalk.red('Usage: gsd worktree merge <name>\n'));
97
134
  process.stderr.write(chalk.dim('Run gsd worktree list to see worktrees.\n'));
98
135
  process.exit(1);
99
136
  }
100
- await doMerge(basePath, name);
137
+ await doMerge(ext, basePath, name);
101
138
  }
102
- async function doMerge(basePath, name) {
103
- const worktrees = listWorktrees(basePath);
139
+ async function doMerge(ext, basePath, name) {
140
+ const worktrees = ext.listWorktrees(basePath);
104
141
  const wt = worktrees.find(w => w.name === name);
105
142
  if (!wt) {
106
143
  process.stderr.write(chalk.red(`Worktree "${name}" not found.\n`));
107
144
  process.exit(1);
108
145
  }
109
- const status = getWorktreeStatus(basePath, name, wt.path);
146
+ const status = getWorktreeStatus(ext, basePath, name, wt.path);
110
147
  if (status.filesChanged === 0 && !status.uncommitted) {
111
148
  process.stderr.write(chalk.dim(`Worktree "${name}" has no changes to merge.\n`));
112
149
  // Clean up empty worktree
113
- removeWorktree(basePath, name, { deleteBranch: true });
150
+ ext.removeWorktree(basePath, name, { deleteBranch: true });
114
151
  process.stderr.write(chalk.green(`Removed empty worktree ${chalk.bold(name)}.\n`));
115
152
  return;
116
153
  }
117
154
  // Auto-commit dirty work before merge
118
155
  if (status.uncommitted) {
119
156
  try {
120
- const { autoCommitCurrentBranch } = await import('./resources/extensions/gsd/worktree.js');
121
- autoCommitCurrentBranch(wt.path, 'worktree-merge', name);
157
+ ext.autoCommitCurrentBranch(wt.path, 'worktree-merge', name);
122
158
  process.stderr.write(chalk.dim(' Auto-committed dirty work before merge.\n'));
123
159
  }
124
160
  catch { /* best-effort */ }
125
161
  }
126
- const commitType = inferCommitType(name);
162
+ const commitType = ext.inferCommitType(name);
127
163
  const commitMessage = `${commitType}(${name}): merge worktree ${name}`;
128
- process.stderr.write(`\nMerging ${chalk.bold.cyan(name)} → ${chalk.magenta(nativeDetectMainBranch(basePath))}\n`);
164
+ process.stderr.write(`\nMerging ${chalk.bold.cyan(name)} → ${chalk.magenta(ext.nativeDetectMainBranch(basePath))}\n`);
129
165
  process.stderr.write(chalk.dim(` ${status.filesChanged} files, ${chalk.green(`+${status.linesAdded}`)} ${chalk.red(`-${status.linesRemoved}`)}\n\n`));
130
166
  try {
131
- mergeWorktreeToMain(basePath, name, commitMessage);
132
- removeWorktree(basePath, name, { deleteBranch: true });
167
+ ext.mergeWorktreeToMain(basePath, name, commitMessage);
168
+ ext.removeWorktree(basePath, name, { deleteBranch: true });
133
169
  process.stderr.write(chalk.green(`✓ Merged and cleaned up ${chalk.bold(name)}\n`));
134
170
  process.stderr.write(chalk.dim(` commit: ${commitMessage}\n`));
135
171
  }
@@ -141,18 +177,19 @@ async function doMerge(basePath, name) {
141
177
  }
142
178
  }
143
179
  // ─── Subcommand: clean ──────────────────────────────────────────────────────
144
- function handleClean(basePath) {
145
- const worktrees = listWorktrees(basePath);
180
+ async function handleClean(basePath) {
181
+ const ext = await loadExtensionModules();
182
+ const worktrees = ext.listWorktrees(basePath);
146
183
  if (worktrees.length === 0) {
147
184
  process.stderr.write(chalk.dim('No worktrees to clean.\n'));
148
185
  return;
149
186
  }
150
187
  let cleaned = 0;
151
188
  for (const wt of worktrees) {
152
- const status = getWorktreeStatus(basePath, wt.name, wt.path);
189
+ const status = getWorktreeStatus(ext, basePath, wt.name, wt.path);
153
190
  if (status.filesChanged === 0 && !status.uncommitted) {
154
191
  try {
155
- removeWorktree(basePath, wt.name, { deleteBranch: true });
192
+ ext.removeWorktree(basePath, wt.name, { deleteBranch: true });
156
193
  process.stderr.write(chalk.green(` ✓ Removed ${chalk.bold(wt.name)} (clean)\n`));
157
194
  cleaned++;
158
195
  }
@@ -167,19 +204,20 @@ function handleClean(basePath) {
167
204
  process.stderr.write(chalk.dim(`\nCleaned ${cleaned} worktree${cleaned === 1 ? '' : 's'}.\n`));
168
205
  }
169
206
  // ─── Subcommand: remove ─────────────────────────────────────────────────────
170
- function handleRemove(basePath, args) {
207
+ async function handleRemove(basePath, args) {
208
+ const ext = await loadExtensionModules();
171
209
  const name = args[0];
172
210
  if (!name) {
173
211
  process.stderr.write(chalk.red('Usage: gsd worktree remove <name>\n'));
174
212
  process.exit(1);
175
213
  }
176
- const worktrees = listWorktrees(basePath);
214
+ const worktrees = ext.listWorktrees(basePath);
177
215
  const wt = worktrees.find(w => w.name === name);
178
216
  if (!wt) {
179
217
  process.stderr.write(chalk.red(`Worktree "${name}" not found.\n`));
180
218
  process.exit(1);
181
219
  }
182
- const status = getWorktreeStatus(basePath, name, wt.path);
220
+ const status = getWorktreeStatus(ext, basePath, name, wt.path);
183
221
  if (status.filesChanged > 0 || status.uncommitted) {
184
222
  process.stderr.write(chalk.yellow(`⚠ Worktree "${name}" has unmerged changes (${status.filesChanged} files).\n`));
185
223
  process.stderr.write(chalk.yellow(' Use --force to remove anyway, or merge first: gsd worktree merge ' + name + '\n'));
@@ -187,17 +225,18 @@ function handleRemove(basePath, args) {
187
225
  process.exit(1);
188
226
  }
189
227
  }
190
- removeWorktree(basePath, name, { deleteBranch: true });
228
+ ext.removeWorktree(basePath, name, { deleteBranch: true });
191
229
  process.stderr.write(chalk.green(`✓ Removed worktree ${chalk.bold(name)}\n`));
192
230
  }
193
231
  // ─── Subcommand: status (default when no args) ─────────────────────────────
194
- function handleStatusBanner(basePath) {
195
- const worktrees = listWorktrees(basePath);
232
+ async function handleStatusBanner(basePath) {
233
+ const ext = await loadExtensionModules();
234
+ const worktrees = ext.listWorktrees(basePath);
196
235
  if (worktrees.length === 0)
197
236
  return;
198
237
  const withChanges = worktrees.filter(wt => {
199
238
  try {
200
- const diff = diffWorktreeAll(basePath, wt.name);
239
+ const diff = ext.diffWorktreeAll(basePath, wt.name);
201
240
  return diff.added.length + diff.modified.length + diff.removed.length > 0;
202
241
  }
203
242
  catch {
@@ -214,14 +253,15 @@ function handleStatusBanner(basePath) {
214
253
  chalk.dim('Resume: gsd -w <name> | Merge: gsd worktree merge <name> | List: gsd worktree list\n\n'));
215
254
  }
216
255
  // ─── -w flag: create/resume worktree for interactive session ────────────────
217
- function handleWorktreeFlag(worktreeFlag) {
256
+ async function handleWorktreeFlag(worktreeFlag) {
257
+ const ext = await loadExtensionModules();
218
258
  const basePath = process.cwd();
219
259
  // gsd -w (no name) — resume most recent worktree with changes, or create new
220
260
  if (worktreeFlag === true) {
221
- const existing = listWorktrees(basePath);
261
+ const existing = ext.listWorktrees(basePath);
222
262
  const withChanges = existing.filter(wt => {
223
263
  try {
224
- const diff = diffWorktreeAll(basePath, wt.name);
264
+ const diff = ext.diffWorktreeAll(basePath, wt.name);
225
265
  return diff.added.length + diff.modified.length + diff.removed.length > 0;
226
266
  }
227
267
  catch {
@@ -243,7 +283,7 @@ function handleWorktreeFlag(worktreeFlag) {
243
283
  // Multiple active worktrees — show them and ask user to pick
244
284
  process.stderr.write(chalk.yellow(`${withChanges.length} worktrees have unmerged changes:\n\n`));
245
285
  for (const wt of withChanges) {
246
- const status = getWorktreeStatus(basePath, wt.name, wt.path);
286
+ const status = getWorktreeStatus(ext, basePath, wt.name, wt.path);
247
287
  process.stderr.write(formatStatus(status) + '\n\n');
248
288
  }
249
289
  process.stderr.write(chalk.dim('Specify which one: gsd -w <name>\n'));
@@ -251,12 +291,12 @@ function handleWorktreeFlag(worktreeFlag) {
251
291
  }
252
292
  // No active worktrees — create a new one
253
293
  const name = generateWorktreeName();
254
- createAndEnter(basePath, name);
294
+ await createAndEnter(ext, basePath, name);
255
295
  return;
256
296
  }
257
297
  // gsd -w <name> — create or resume named worktree
258
298
  const name = worktreeFlag;
259
- const existing = listWorktrees(basePath);
299
+ const existing = ext.listWorktrees(basePath);
260
300
  const found = existing.find(wt => wt.name === name);
261
301
  if (found) {
262
302
  process.chdir(found.path);
@@ -267,13 +307,13 @@ function handleWorktreeFlag(worktreeFlag) {
267
307
  process.stderr.write(chalk.dim(` branch ${found.branch}\n\n`));
268
308
  }
269
309
  else {
270
- createAndEnter(basePath, name);
310
+ await createAndEnter(ext, basePath, name);
271
311
  }
272
312
  }
273
- function createAndEnter(basePath, name) {
313
+ async function createAndEnter(ext, basePath, name) {
274
314
  try {
275
- const info = createWorktree(basePath, name);
276
- const hookError = runWorktreePostCreateHook(basePath, info.path);
315
+ const info = ext.createWorktree(basePath, name);
316
+ const hookError = ext.runWorktreePostCreateHook(basePath, info.path);
277
317
  if (hookError) {
278
318
  process.stderr.write(chalk.yellow(`[gsd] ${hookError}\n`));
279
319
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-pi",
3
- "version": "2.31.2-dev.2453512",
3
+ "version": "2.31.2-dev.392f611",
4
4
  "description": "GSD — Get Shit Done coding agent",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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")));