gsd-lite 0.6.1 → 0.6.3

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.
@@ -13,7 +13,7 @@
13
13
  "name": "gsd",
14
14
  "source": "./",
15
15
  "description": "AI orchestration tool — GSD management shell + Superpowers quality core. 5 commands, 4 agents, 5 workflows, MCP server, context monitoring.",
16
- "version": "0.6.1",
16
+ "version": "0.6.3",
17
17
  "keywords": [
18
18
  "orchestration",
19
19
  "mcp",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "AI orchestration tool for Claude Code — GSD management shell + Superpowers quality core",
5
5
  "author": {
6
6
  "name": "sdsrss",
@@ -286,6 +286,7 @@ async function fetchLatestRelease(token) {
286
286
  if (!res.ok) return null;
287
287
 
288
288
  const data = await res.json();
289
+ if (!data.tag_name || !data.tarball_url) return null;
289
290
  return {
290
291
  version: data.tag_name.replace(/^v/, ''),
291
292
  tarballUrl: data.tarball_url,
@@ -13,10 +13,8 @@ const fs = require('node:fs');
13
13
  const path = require('node:path');
14
14
  const os = require('node:os');
15
15
 
16
- const pluginRoot = path.resolve(__dirname, '..');
17
16
  const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
18
17
  const settingsPath = path.join(claudeDir, 'settings.json');
19
- const statuslineScript = path.join(pluginRoot, 'hooks', 'gsd-statusline.cjs');
20
18
 
21
19
  // Safety: exit after 4s regardless (hook timeout is 5s)
22
20
  setTimeout(() => process.exit(0), 4000).unref();
@@ -49,21 +47,38 @@ setTimeout(() => process.exit(0), 4000).unref();
49
47
  } catch { /* silent */ }
50
48
 
51
49
  // ── Phase 2: StatusLine auto-registration ──
50
+ // StatusLine is a top-level settings.json config that the plugin system
51
+ // (hooks.json) cannot manage. Self-heal if not registered.
52
52
  try {
53
- if (fs.existsSync(statuslineScript)) {
53
+ const stableStatuslinePath = path.join(claudeDir, 'hooks', 'gsd-statusline.cjs');
54
+ if (fs.existsSync(stableStatuslinePath)) {
54
55
  let settings = {};
55
56
  try {
56
57
  settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
57
58
  } catch { /* Can't read settings — skip registration */ }
58
59
 
59
- if (settings && !settings.statusLine?.command) {
60
- settings.statusLine = {
61
- type: 'command',
62
- command: `node ${JSON.stringify(statuslineScript)}`
63
- };
64
- const tmpPath = settingsPath + `.gsd-tmp-${process.pid}`;
65
- fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n');
66
- fs.renameSync(tmpPath, settingsPath);
60
+ if (settings) {
61
+ const current = settings.statusLine?.command || '';
62
+
63
+ if (current.includes('gsd-statusline')) {
64
+ // Already registered — nothing to do
65
+ } else if (!current) {
66
+ // No statusLine register directly
67
+ settings.statusLine = {
68
+ type: 'command',
69
+ command: `node ${JSON.stringify(stableStatuslinePath)}`
70
+ };
71
+ const tmpPath = settingsPath + `.gsd-tmp-${process.pid}`;
72
+ fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n');
73
+ fs.renameSync(tmpPath, settingsPath);
74
+ } else if (current.includes('statusline-composite')) {
75
+ // Composite system (e.g., code-graph) — register as provider
76
+ try {
77
+ const { registerProvider } = require('./lib/statusline-composite.cjs');
78
+ registerProvider(stableStatuslinePath);
79
+ } catch { /* composite helper not available */ }
80
+ }
81
+ // else: some other statusLine, don't overwrite
67
82
  }
68
83
  }
69
84
  } catch { /* silent */ }
@@ -0,0 +1,110 @@
1
+ 'use strict';
2
+ // Detect and register with composite statusline systems (e.g., code-graph).
3
+ // Used by install.js, gsd-session-init.cjs, and uninstall.js.
4
+
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const os = require('node:os');
8
+
9
+ // Known composite statusline registry paths
10
+ const REGISTRY_PATHS = [
11
+ path.join(os.homedir(), '.cache', 'code-graph', 'statusline-registry.json'),
12
+ ];
13
+
14
+ function isCompositeStatusLine(command) {
15
+ return typeof command === 'string' && command.includes('statusline-composite');
16
+ }
17
+
18
+ function findCompositeRegistry() {
19
+ for (const p of REGISTRY_PATHS) {
20
+ if (fs.existsSync(p)) return p;
21
+ }
22
+ return null;
23
+ }
24
+
25
+ /**
26
+ * Register GSD as a provider in the composite statusline registry.
27
+ * Idempotent: updates existing entry or inserts before code-graph.
28
+ * @param {string} statuslineScriptPath - Absolute path to gsd-statusline.cjs
29
+ * @returns {boolean} true if registered/updated
30
+ */
31
+ function registerProvider(statuslineScriptPath) {
32
+ let registryPath = findCompositeRegistry();
33
+
34
+ // If composite statusLine is configured but registry file is missing,
35
+ // create it if the parent directory exists (e.g., code-graph installed
36
+ // but registry was deleted or not yet created).
37
+ if (!registryPath) {
38
+ for (const candidate of REGISTRY_PATHS) {
39
+ const dir = path.dirname(candidate);
40
+ if (fs.existsSync(dir)) {
41
+ registryPath = candidate;
42
+ break;
43
+ }
44
+ }
45
+ if (!registryPath) return false;
46
+ }
47
+
48
+ try {
49
+ let registry;
50
+ try {
51
+ registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
52
+ } catch {
53
+ registry = []; // File missing or corrupt — start fresh
54
+ }
55
+ if (!Array.isArray(registry)) return false;
56
+
57
+ const command = `node ${JSON.stringify(statuslineScriptPath)}`;
58
+ const provider = { id: 'gsd', command, needsStdin: true };
59
+
60
+ // Find existing GSD entry (by id or command)
61
+ const idx = registry.findIndex(p =>
62
+ p.id === 'gsd' || p.command?.includes('gsd-statusline'));
63
+
64
+ if (idx >= 0) {
65
+ registry[idx] = provider;
66
+ } else {
67
+ // Insert before code-graph for display priority
68
+ const cgIdx = registry.findIndex(p => p.id === 'code-graph');
69
+ if (cgIdx >= 0) registry.splice(cgIdx, 0, provider);
70
+ else registry.unshift(provider);
71
+ }
72
+
73
+ // Atomic write
74
+ const tmp = registryPath + `.${process.pid}-${Date.now()}.tmp`;
75
+ fs.writeFileSync(tmp, JSON.stringify(registry, null, 2) + '\n');
76
+ fs.renameSync(tmp, registryPath);
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Remove GSD entry from composite statusline registry.
85
+ * @returns {boolean} true if an entry was removed
86
+ */
87
+ function removeProvider() {
88
+ const registryPath = findCompositeRegistry();
89
+ if (!registryPath) return false;
90
+
91
+ try {
92
+ const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
93
+ if (!Array.isArray(registry)) return false;
94
+
95
+ const idx = registry.findIndex(p =>
96
+ p.id === 'gsd' || p.command?.includes('gsd-statusline'));
97
+ if (idx < 0) return false;
98
+
99
+ registry.splice(idx, 1);
100
+
101
+ const tmp = registryPath + `.${process.pid}-${Date.now()}.tmp`;
102
+ fs.writeFileSync(tmp, JSON.stringify(registry, null, 2) + '\n');
103
+ fs.renameSync(tmp, registryPath);
104
+ return true;
105
+ } catch {
106
+ return false;
107
+ }
108
+ }
109
+
110
+ module.exports = { isCompositeStatusLine, findCompositeRegistry, registerProvider, removeProvider };
package/install.js CHANGED
@@ -11,6 +11,7 @@ import { createRequire } from 'node:module';
11
11
  const __dirname = dirname(fileURLToPath(import.meta.url));
12
12
  const _require = createRequire(import.meta.url);
13
13
  const { semverSortComparator } = _require('./hooks/lib/semver-sort.cjs');
14
+ const { isCompositeStatusLine, registerProvider: registerCompositeProvider } = _require('./hooks/lib/statusline-composite.cjs');
14
15
  const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
15
16
  const RUNTIME_DIR = join(CLAUDE_DIR, 'gsd');
16
17
  const DRY_RUN = process.argv.includes('--dry-run');
@@ -39,16 +40,35 @@ function isInstalledAsPlugin(claudeDir) {
39
40
 
40
41
  function registerStatusLine(settings, statuslineScriptPath) {
41
42
  const command = `node ${JSON.stringify(statuslineScriptPath)}`;
42
- // Don't overwrite non-GSD statusLine
43
- if (settings.statusLine && typeof settings.statusLine === 'object'
44
- && !settings.statusLine.command?.includes('gsd-statusline')) {
45
- log(' ! Preserved existing statusLine');
46
- return false;
47
- }
48
- settings.statusLine = { type: 'command', command };
43
+
49
44
  // Clean up legacy format (was incorrectly placed in hooks)
50
45
  if (settings.hooks?.StatusLine) delete settings.hooks.StatusLine;
51
- return true;
46
+
47
+ const current = settings.statusLine?.command || '';
48
+
49
+ // Already GSD → update command path
50
+ if (current.includes('gsd-statusline')) {
51
+ settings.statusLine = { type: 'command', command };
52
+ return true;
53
+ }
54
+
55
+ // No statusLine → set GSD directly
56
+ if (!current) {
57
+ settings.statusLine = { type: 'command', command };
58
+ return true;
59
+ }
60
+
61
+ // Composite statusLine (e.g., code-graph) → register as provider
62
+ if (isCompositeStatusLine(current)) {
63
+ if (registerCompositeProvider(statuslineScriptPath)) {
64
+ log(' ✓ Registered GSD in composite statusLine registry');
65
+ return true;
66
+ }
67
+ }
68
+
69
+ // Other statusLine → don't overwrite
70
+ log(' ! Preserved existing statusLine');
71
+ return false;
52
72
  }
53
73
 
54
74
  function registerHookEntry(hooks, { hookType, identifier, matcher, timeout }) {
@@ -170,8 +190,13 @@ export function main() {
170
190
  copyDir(localNM, join(RUNTIME_DIR, 'node_modules'), 'runtime/node_modules (copied)');
171
191
  } else if (!DRY_RUN) {
172
192
  log(' ⧗ Installing runtime dependencies...');
173
- execSync('npm ci --omit=dev', { cwd: RUNTIME_DIR, stdio: 'pipe' });
174
- log(' runtime dependencies installed');
193
+ try {
194
+ execSync('npm ci --omit=dev', { cwd: RUNTIME_DIR, stdio: 'pipe' });
195
+ log(' ✓ runtime dependencies installed');
196
+ } catch (err) {
197
+ log(` ✗ Failed to install runtime dependencies: ${err.message}`);
198
+ process.exit(1);
199
+ }
175
200
  } else {
176
201
  log(' [dry-run] Would install runtime dependencies');
177
202
  }
@@ -209,15 +234,16 @@ export function main() {
209
234
  log(' ✓ MCP server registered in settings.json');
210
235
  }
211
236
 
212
- // Register statusLine (top-level setting) and hooks
213
- // When installed as a plugin, hooks are managed by hooks.json via the plugin system.
237
+ // StatusLine is a top-level setting that the plugin system (hooks.json)
238
+ // cannot manage. Always register, regardless of install method.
239
+ const statuslinePath = join(CLAUDE_DIR, 'hooks', 'gsd-statusline.cjs');
240
+ let statusLineRegistered = registerStatusLine(settings, statuslinePath);
241
+
242
+ // Hooks are managed by hooks.json via the plugin system for plugin installs.
214
243
  // Only register in settings.json for manual installs to avoid double execution.
215
- let statusLineRegistered = false;
216
244
  let hooksRegistered = false;
217
245
  if (!isPluginInstall) {
218
246
  if (!settings.hooks) settings.hooks = {};
219
- const statuslinePath = join(CLAUDE_DIR, 'hooks', 'gsd-statusline.cjs');
220
- statusLineRegistered = registerStatusLine(settings, statuslinePath);
221
247
  for (const config of HOOK_REGISTRY) {
222
248
  if (registerHookEntry(settings.hooks, config)) hooksRegistered = true;
223
249
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-lite",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "AI orchestration tool for Claude Code — GSD management shell + Superpowers quality core",
5
5
  "type": "module",
6
6
  "bin": {
@@ -208,20 +208,11 @@ async function evaluatePreflight(state, basePath) {
208
208
  }
209
209
  }
210
210
 
211
- const expired_research = collectExpiredResearch(state);
212
- if (expired_research.length > 0) {
213
- hints.push({
214
- workflow_mode: 'research_refresh_needed',
215
- action: 'dispatch_researcher',
216
- updates: { workflow_mode: 'research_refresh_needed' },
217
- expired_research,
218
- message: 'Research cache expired and must be refreshed before execution resumes',
219
- });
220
- }
221
-
222
- // P0-2: Dirty-phase detection — rollback current_phase to earliest phase
211
+ // Dirty-phase detection — rollback current_phase to earliest phase
223
212
  // that has needs_revalidation tasks, ensuring earlier invalidated work
224
213
  // is re-executed before proceeding with later phases.
214
+ // Priority: placed before research expiry because dirty-phase rollback is a
215
+ // safety-critical action (prevents executing later phases on stale foundations).
225
216
  // Use filter+reduce (not .find) to guarantee lowest-ID match regardless of array order.
226
217
  const dirtyPhases = (state.phases || []).filter(p =>
227
218
  p.id < state.current_phase
@@ -245,6 +236,17 @@ async function evaluatePreflight(state, basePath) {
245
236
  });
246
237
  }
247
238
 
239
+ const expired_research = collectExpiredResearch(state);
240
+ if (expired_research.length > 0) {
241
+ hints.push({
242
+ workflow_mode: 'research_refresh_needed',
243
+ action: 'dispatch_researcher',
244
+ updates: { workflow_mode: 'research_refresh_needed' },
245
+ expired_research,
246
+ message: 'Research cache expired and must be refreshed before execution resumes',
247
+ });
248
+ }
249
+
248
250
  if (hints.length === 0) return { override: null };
249
251
 
250
252
  return {
@@ -53,7 +53,7 @@ function _buildResumeSummary(state, response) {
53
53
  };
54
54
  }
55
55
 
56
- async function resumeAwaitingClear(state, basePath) {
56
+ async function resumeAwaitingClear(state, basePath, _depth = 0) {
57
57
  const health = await readContextHealth(basePath);
58
58
  if (health !== null && health < CONTEXT_RESUME_THRESHOLD) {
59
59
  const persistError = await persist(basePath, {
@@ -83,7 +83,7 @@ async function resumeAwaitingClear(state, basePath) {
83
83
  }
84
84
  const persistError = await persist(basePath, updates);
85
85
  if (persistError) return persistError;
86
- return resumeWorkflow({ basePath });
86
+ return resumeWorkflow({ basePath, _depth: _depth + 1 });
87
87
  }
88
88
 
89
89
  async function resumeExecutingTask(state, basePath) {
@@ -319,7 +319,7 @@ export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0, unb
319
319
  result = await resumeExecutingTask(state, basePath);
320
320
  break;
321
321
  case 'awaiting_clear':
322
- result = await resumeAwaitingClear(state, basePath);
322
+ result = await resumeAwaitingClear(state, basePath, _depth);
323
323
  break;
324
324
  case 'awaiting_user': {
325
325
  if (state.current_review?.stage === 'direction_drift') {
@@ -74,6 +74,23 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
74
74
  const qualityFailed = result.quality_passed === false;
75
75
  const needsRework = hasCritical || specFailed || qualityFailed;
76
76
 
77
+ // Safety: if rework is needed but no tasks were targeted for rework,
78
+ // fall back to marking all non-accepted checkpointed/accepted tasks as needs_revalidation
79
+ // to prevent infinite review loops (no runnable tasks → trigger_review → same result).
80
+ if (needsRework && taskPatches.filter(p => p.lifecycle === 'needs_revalidation').length === 0) {
81
+ for (const task of (phase.todo || [])) {
82
+ if (task.lifecycle === 'checkpointed' || task.lifecycle === 'accepted') {
83
+ taskPatches.push({
84
+ id: task.id,
85
+ lifecycle: 'needs_revalidation',
86
+ retry_count: 0,
87
+ evidence_refs: [],
88
+ last_review_feedback: ['Reviewer indicated rework needed but did not specify tasks; all completed tasks require revalidation'],
89
+ });
90
+ }
91
+ }
92
+ }
93
+
77
94
  // Compute retry count once for both exhaustion check and state update
78
95
  const currentRetryCount = phase.phase_review?.retry_count || 0;
79
96
  const nextRetryCount = needsRework ? currentRetryCount + 1 : 0;
@@ -251,6 +251,11 @@ export async function update({ updates, basePath = process.cwd(), expectedVersio
251
251
  // Deep merge phases by ID instead of shallow replace [I-1]
252
252
  const merged = { ...state, ...updates };
253
253
 
254
+ // Deep merge context by key (preserves plan_hashes, last_session, etc.)
255
+ if (updates.context && isPlainObject(updates.context)) {
256
+ merged.context = { ...(state.context || {}), ...updates.context };
257
+ }
258
+
254
259
  // Deep merge evidence by key (preserves existing entries)
255
260
  if (updates.evidence && isPlainObject(updates.evidence)) {
256
261
  merged.evidence = { ...(state.evidence || {}), ...updates.evidence };
@@ -326,10 +331,13 @@ export async function update({ updates, basePath = process.cwd(), expectedVersio
326
331
  await _pruneEvidenceFromState(merged, merged.current_phase, gsdDir);
327
332
  }
328
333
 
329
- // Use incremental validation for simple updates (no phases changes)
330
- const validation = !updates.phases
331
- ? validateStateUpdate(state, updates)
332
- : validateState(merged);
334
+ // Use incremental validation for simple updates (no phases/propagation/decisions changes)
335
+ const needsFullValidation = updates.phases
336
+ || (_append_decisions?.length > 0)
337
+ || (_propagation_tasks?.length > 0);
338
+ const validation = needsFullValidation
339
+ ? validateState(merged)
340
+ : validateStateUpdate(state, updates);
333
341
  if (!validation.valid) {
334
342
  return {
335
343
  error: true,
@@ -495,6 +503,7 @@ export async function phaseComplete({
495
503
  if (!driftValidation.valid) {
496
504
  return { error: true, code: ERROR_CODES.VALIDATION_FAILED, message: `Validation failed: ${driftValidation.errors.join('; ')}` };
497
505
  }
506
+ state._version = (state._version ?? 0) + 1;
498
507
  await writeJson(statePath, state);
499
508
  return {
500
509
  success: true,
@@ -525,6 +534,8 @@ export async function phaseComplete({
525
534
  } else if (state.current_phase === phase_id && phase_id >= state.total_phases) {
526
535
  // Final phase completed — mark workflow as completed
527
536
  state.workflow_mode = 'completed';
537
+ state.current_task = null;
538
+ state.current_review = null;
528
539
  }
529
540
 
530
541
  // Update git_head to current commit
@@ -539,6 +550,7 @@ export async function phaseComplete({
539
550
  if (!finalValidation.valid) {
540
551
  return { error: true, code: ERROR_CODES.VALIDATION_FAILED, message: `Validation failed: ${finalValidation.errors.join('; ')}` };
541
552
  }
553
+ state._version = (state._version ?? 0) + 1;
542
554
  await writeJson(statePath, state);
543
555
  return { success: true };
544
556
  });
@@ -570,7 +582,7 @@ export async function addEvidence({ id, data, basePath = process.cwd() }) {
570
582
  if (!result.ok) {
571
583
  return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
572
584
  }
573
- const state = result.data;
585
+ const state = migrateState(result.data);
574
586
 
575
587
  if (!state.evidence) {
576
588
  state.evidence = {};
@@ -585,6 +597,7 @@ export async function addEvidence({ id, data, basePath = process.cwd() }) {
585
597
  await _pruneEvidenceFromState(state, state.current_phase, gsdDir);
586
598
  }
587
599
 
600
+ state._version = (state._version ?? 0) + 1;
588
601
  await writeJson(statePath, state);
589
602
  return { success: true };
590
603
  });
@@ -650,11 +663,14 @@ export async function pruneEvidence({ currentPhase, basePath = process.cwd() })
650
663
  if (!result.ok) {
651
664
  return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
652
665
  }
653
- const state = result.data;
666
+ const state = migrateState(result.data);
654
667
 
655
668
  const gsdDir = dirname(statePath);
656
669
  const archived = await _pruneEvidenceFromState(state, currentPhase, gsdDir);
657
- if (archived > 0) await writeJson(statePath, state);
670
+ if (archived > 0) {
671
+ state._version = (state._version ?? 0) + 1;
672
+ await writeJson(statePath, state);
673
+ }
658
674
 
659
675
  return { success: true, archived };
660
676
  });
@@ -734,6 +750,7 @@ export async function patchPlan({ operations, basePath = process.cwd() } = {}) {
734
750
  return { error: true, code: ERROR_CODES.VALIDATION_FAILED, message: `Validation failed: ${validation.errors.join('; ')}` };
735
751
  }
736
752
 
753
+ state._version = (state._version ?? 0) + 1;
737
754
  await writeJson(statePath, state);
738
755
  return { success: true, applied, plan_version: state.plan_version };
739
756
  });
@@ -5,6 +5,7 @@ import { writeFile, rename, unlink } from 'node:fs/promises';
5
5
  import { ensureDir, readJson, writeJson, getStatePath } from '../../utils.js';
6
6
  import {
7
7
  TASK_LIFECYCLE,
8
+ migrateState,
8
9
  validateResearchArtifacts,
9
10
  validateResearchDecisionIndex,
10
11
  validateResearcherResult,
@@ -439,7 +440,7 @@ export async function storeResearch({ result, artifacts, decision_index, basePat
439
440
  return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: current.error };
440
441
  }
441
442
 
442
- const state = current.data;
443
+ const state = migrateState(current.data);
443
444
  const gsdDir = dirname(statePath);
444
445
  const researchDir = join(gsdDir, 'research');
445
446
  await ensureDir(researchDir);
@@ -503,6 +504,7 @@ export async function storeResearch({ result, artifacts, decision_index, basePat
503
504
  return { error: true, code: ERROR_CODES.VALIDATION_FAILED, message: `State validation failed: ${validation.errors.join('; ')}` };
504
505
  }
505
506
 
507
+ state._version = (state._version ?? 0) + 1;
506
508
  await writeJson(statePath, state);
507
509
  return {
508
510
  success: true,
package/uninstall.js CHANGED
@@ -5,6 +5,7 @@ import { existsSync, rmSync, readFileSync, writeFileSync, renameSync } from 'nod
5
5
  import { join } from 'node:path';
6
6
  import { homedir } from 'node:os';
7
7
  import { pathToFileURL } from 'node:url';
8
+ import { createRequire } from 'node:module';
8
9
 
9
10
  const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
10
11
  const RUNTIME_DIR = join(CLAUDE_DIR, 'gsd');
@@ -27,6 +28,16 @@ function removeDir(path, label) {
27
28
  export function main() {
28
29
  log('GSD-Lite Uninstaller\n');
29
30
 
31
+ // Clean up GSD entry from composite statusLine registry before removing files
32
+ try {
33
+ const _require = createRequire(import.meta.url);
34
+ const compositeLib = join(CLAUDE_DIR, 'hooks', 'lib', 'statusline-composite.cjs');
35
+ if (existsSync(compositeLib)) {
36
+ const { removeProvider } = _require(compositeLib);
37
+ if (removeProvider()) log(' ✓ Removed GSD from composite statusLine registry');
38
+ }
39
+ } catch { /* best effort */ }
40
+
30
41
  log('Removing files...');
31
42
 
32
43
  removeDir(join(CLAUDE_DIR, 'commands', 'gsd'), 'commands/gsd/');
@@ -49,10 +60,12 @@ export function main() {
49
60
  const hookLibDir = join(CLAUDE_DIR, 'hooks', 'lib');
50
61
  if (existsSync(hookLibDir)) {
51
62
  // Only remove GSD-owned files, not other plugins' libs
52
- const gsdLibFile = join(hookLibDir, 'gsd-finder.cjs');
53
- if (existsSync(gsdLibFile)) {
54
- rmSync(gsdLibFile);
55
- log(' ✓ Removed hooks/lib/gsd-finder.cjs');
63
+ for (const libFile of ['gsd-finder.cjs', 'statusline-composite.cjs', 'semver-sort.cjs']) {
64
+ const fullPath = join(hookLibDir, libFile);
65
+ if (existsSync(fullPath)) {
66
+ rmSync(fullPath);
67
+ log(` ✓ Removed hooks/lib/${libFile}`);
68
+ }
56
69
  }
57
70
  }
58
71