gsd-lite 0.2.0 → 0.3.1

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/src/server.js CHANGED
@@ -2,7 +2,11 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
4
4
  import { pathToFileURL } from 'node:url';
5
+ import { createRequire } from 'node:module';
5
6
  import { init, read, update, phaseComplete } from './tools/state.js';
7
+
8
+ const _require = createRequire(import.meta.url);
9
+ const PKG_VERSION = _require('../package.json').version;
6
10
  import {
7
11
  handleDebuggerResult,
8
12
  handleExecutorResult,
@@ -12,13 +16,13 @@ import {
12
16
  } from './tools/orchestrator.js';
13
17
 
14
18
  const server = new Server(
15
- { name: 'gsd-lite', version: '0.2.0' },
19
+ { name: 'gsd', version: PKG_VERSION },
16
20
  { capabilities: { tools: {} } }
17
21
  );
18
22
 
19
23
  const TOOLS = [
20
24
  {
21
- name: 'gsd-health',
25
+ name: 'health',
22
26
  description: 'Health check: returns server status and whether .gsd state exists',
23
27
  inputSchema: {
24
28
  type: 'object',
@@ -26,7 +30,7 @@ const TOOLS = [
26
30
  },
27
31
  },
28
32
  {
29
- name: 'gsd-state-init',
33
+ name: 'state-init',
30
34
  description: 'Initialize .gsd/ directory with state.json, plan.md, and phases/*.md',
31
35
  inputSchema: {
32
36
  type: 'object',
@@ -38,9 +42,25 @@ const TOOLS = [
38
42
  items: {
39
43
  type: 'object',
40
44
  properties: {
41
- name: { type: 'string' },
42
- tasks: { type: 'array' },
45
+ name: { type: 'string', description: 'Phase name' },
46
+ tasks: {
47
+ type: 'array',
48
+ description: 'Task definitions',
49
+ items: {
50
+ type: 'object',
51
+ properties: {
52
+ name: { type: 'string', description: 'Task name (required)' },
53
+ index: { type: 'number', description: 'Task index within phase (default: auto)' },
54
+ level: { type: 'string', description: 'Complexity level: L0/L1/L2/L3 (default: L1)' },
55
+ requires: { type: 'array', description: 'Dependency list (default: [])' },
56
+ review_required: { type: 'boolean', description: 'Whether review is needed (default: true)' },
57
+ verification_required: { type: 'boolean', description: 'Whether verification is needed (default: true)' },
58
+ },
59
+ required: ['name'],
60
+ },
61
+ },
43
62
  },
63
+ required: ['name'],
44
64
  },
45
65
  },
46
66
  research: { type: 'boolean', description: 'Whether research directory is needed' },
@@ -49,7 +69,7 @@ const TOOLS = [
49
69
  },
50
70
  },
51
71
  {
52
- name: 'gsd-state-read',
72
+ name: 'state-read',
53
73
  description: 'Read state.json, optionally filtering to specific fields',
54
74
  inputSchema: {
55
75
  type: 'object',
@@ -63,7 +83,7 @@ const TOOLS = [
63
83
  },
64
84
  },
65
85
  {
66
- name: 'gsd-state-update',
86
+ name: 'state-update',
67
87
  description: 'Update state.json canonical fields with lifecycle validation',
68
88
  inputSchema: {
69
89
  type: 'object',
@@ -77,7 +97,7 @@ const TOOLS = [
77
97
  },
78
98
  },
79
99
  {
80
- name: 'gsd-phase-complete',
100
+ name: 'phase-complete',
81
101
  description: 'Mark a phase as complete after verifying handoff gate conditions',
82
102
  inputSchema: {
83
103
  type: 'object',
@@ -100,7 +120,7 @@ const TOOLS = [
100
120
  },
101
121
  },
102
122
  {
103
- name: 'gsd-orchestrator-resume',
123
+ name: 'orchestrator-resume',
104
124
  description: 'Resume the minimal orchestration loop from workflow_mode/current_phase state',
105
125
  inputSchema: {
106
126
  type: 'object',
@@ -108,7 +128,7 @@ const TOOLS = [
108
128
  },
109
129
  },
110
130
  {
111
- name: 'gsd-orchestrator-handle-executor-result',
131
+ name: 'orchestrator-handle-executor-result',
112
132
  description: 'Persist an executor result and determine the next orchestration action',
113
133
  inputSchema: {
114
134
  type: 'object',
@@ -119,7 +139,7 @@ const TOOLS = [
119
139
  },
120
140
  },
121
141
  {
122
- name: 'gsd-orchestrator-handle-debugger-result',
142
+ name: 'orchestrator-handle-debugger-result',
123
143
  description: 'Persist a debugger result and determine the next orchestration action',
124
144
  inputSchema: {
125
145
  type: 'object',
@@ -130,7 +150,7 @@ const TOOLS = [
130
150
  },
131
151
  },
132
152
  {
133
- name: 'gsd-orchestrator-handle-researcher-result',
153
+ name: 'orchestrator-handle-researcher-result',
134
154
  description: 'Persist a researcher result, write .gsd/research artifacts, and continue orchestration',
135
155
  inputSchema: {
136
156
  type: 'object',
@@ -143,7 +163,7 @@ const TOOLS = [
143
163
  },
144
164
  },
145
165
  {
146
- name: 'gsd-orchestrator-handle-reviewer-result',
166
+ name: 'orchestrator-handle-reviewer-result',
147
167
  description: 'Persist a reviewer result, update task lifecycles, and determine next orchestration action',
148
168
  inputSchema: {
149
169
  type: 'object',
@@ -162,12 +182,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
162
182
  async function dispatchToolCall(name, args) {
163
183
  let result;
164
184
  switch (name) {
165
- case 'gsd-health': {
185
+ case 'health': {
166
186
  const stateResult = await read(args || {});
167
187
  result = {
168
188
  status: 'ok',
169
- server: 'gsd-lite',
170
- version: '0.2.0',
189
+ server: 'gsd',
190
+ version: PKG_VERSION,
171
191
  state_exists: !stateResult.error,
172
192
  ...(stateResult.error ? {} : {
173
193
  project: stateResult.project,
@@ -178,31 +198,31 @@ async function dispatchToolCall(name, args) {
178
198
  };
179
199
  break;
180
200
  }
181
- case 'gsd-state-init':
201
+ case 'state-init':
182
202
  result = await init(args);
183
203
  break;
184
- case 'gsd-state-read':
204
+ case 'state-read':
185
205
  result = await read(args || {});
186
206
  break;
187
- case 'gsd-state-update':
207
+ case 'state-update':
188
208
  result = await update(args);
189
209
  break;
190
- case 'gsd-phase-complete':
210
+ case 'phase-complete':
191
211
  result = await phaseComplete(args);
192
212
  break;
193
- case 'gsd-orchestrator-resume':
213
+ case 'orchestrator-resume':
194
214
  result = await resumeWorkflow(args || {});
195
215
  break;
196
- case 'gsd-orchestrator-handle-executor-result':
216
+ case 'orchestrator-handle-executor-result':
197
217
  result = await handleExecutorResult(args || {});
198
218
  break;
199
- case 'gsd-orchestrator-handle-debugger-result':
219
+ case 'orchestrator-handle-debugger-result':
200
220
  result = await handleDebuggerResult(args || {});
201
221
  break;
202
- case 'gsd-orchestrator-handle-researcher-result':
222
+ case 'orchestrator-handle-researcher-result':
203
223
  result = await handleResearcherResult(args || {});
204
224
  break;
205
- case 'gsd-orchestrator-handle-reviewer-result':
225
+ case 'orchestrator-handle-reviewer-result':
206
226
  result = await handleReviewerResult(args || {});
207
227
  break;
208
228
  default:
@@ -106,7 +106,7 @@ async function evaluatePreflight(state, basePath) {
106
106
  return { override: null };
107
107
  }
108
108
 
109
- const currentGitHead = getGitHead(basePath);
109
+ const currentGitHead = await getGitHead(basePath);
110
110
  if (state.git_head && currentGitHead && state.git_head !== currentGitHead) {
111
111
  return {
112
112
  override: {
@@ -386,6 +386,7 @@ async function resumeExecutingTask(state, basePath) {
386
386
  if (state.current_task) {
387
387
  const currentTask = getTaskById(phase, state.current_task);
388
388
  if (currentTask?.lifecycle === 'running') {
389
+ const isRetrying = (currentTask.retry_count || 0) > 0;
389
390
  const persistError = await persist(basePath, {
390
391
  workflow_mode: 'executing_task',
391
392
  current_task: currentTask.id,
@@ -394,7 +395,12 @@ async function resumeExecutingTask(state, basePath) {
394
395
  if (persistError) return persistError;
395
396
  return buildExecutorDispatch(state, phase, currentTask, {
396
397
  resumed: true,
397
- interruption_recovered: true,
398
+ interruption_recovered: !isRetrying,
399
+ ...(isRetrying ? {
400
+ retry_after_failure: true,
401
+ retry_count: currentTask.retry_count,
402
+ last_failure_summary: currentTask.last_failure_summary,
403
+ } : {}),
398
404
  });
399
405
  }
400
406
  }
@@ -419,11 +425,16 @@ async function resumeExecutingTask(state, basePath) {
419
425
 
420
426
  if (selection.mode === 'trigger_review') {
421
427
  const current_review = { scope: 'phase', scope_id: phase.id };
422
- const persistError = await persist(basePath, {
428
+ const updates = {
423
429
  workflow_mode: 'reviewing_phase',
424
430
  current_task: null,
425
431
  current_review,
426
- });
432
+ };
433
+ // Auto-advance phase lifecycle to 'reviewing' if currently 'active'
434
+ if (phase.lifecycle === 'active') {
435
+ updates.phases = [{ id: phase.id, lifecycle: 'reviewing' }];
436
+ }
437
+ const persistError = await persist(basePath, updates);
427
438
  if (persistError) return persistError;
428
439
 
429
440
  return {
@@ -562,6 +573,7 @@ export async function resumeWorkflow({ basePath = process.cwd() } = {}) {
562
573
  id: task.id,
563
574
  level: task.level,
564
575
  checkpoint_commit: task.checkpoint_commit || null,
576
+ files_changed: task.files_changed || [],
565
577
  })),
566
578
  };
567
579
  }
@@ -880,6 +892,7 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
880
892
 
881
893
  const taskPatches = [];
882
894
  let doneIncrement = 0;
895
+ let doneDecrement = 0;
883
896
 
884
897
  // Accept tasks
885
898
  for (const taskId of (result.accepted_tasks || [])) {
@@ -896,6 +909,7 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
896
909
  const task = getTaskById(phase, taskId);
897
910
  if (!task) continue;
898
911
  if (task.lifecycle === 'checkpointed' || task.lifecycle === 'accepted') {
912
+ if (task.lifecycle === 'accepted') doneDecrement += 1;
899
913
  taskPatches.push({ id: taskId, lifecycle: 'needs_revalidation', evidence_refs: [] });
900
914
  }
901
915
  }
@@ -919,7 +933,7 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
919
933
 
920
934
  const phaseUpdates = {
921
935
  id: phase.id,
922
- done: (phase.done || 0) + doneIncrement,
936
+ done: Math.max(0, (phase.done || 0) + doneIncrement - doneDecrement),
923
937
  phase_review: {
924
938
  status: reviewStatus,
925
939
  ...(hasCritical ? { retry_count: (phase.phase_review?.retry_count || 0) + 1 } : {}),
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { join, dirname } from 'node:path';
4
4
  import { stat } from 'node:fs/promises';
5
- import { ensureDir, readJson, writeJson, writeAtomic, getStatePath, getGitHead } from '../utils.js';
5
+ import { ensureDir, readJson, writeJson, writeAtomic, getStatePath, getGitHead, isPlainObject } from '../utils.js';
6
6
  import {
7
7
  CANONICAL_FIELDS,
8
8
  TASK_LIFECYCLE,
@@ -25,10 +25,6 @@ function withStateLock(fn) {
25
25
  return p;
26
26
  }
27
27
 
28
- function isPlainObject(value) {
29
- return value !== null && typeof value === 'object' && !Array.isArray(value);
30
- }
31
-
32
28
  function inferWorkflowModeAfterResearch(state) {
33
29
  if (state.current_review?.scope === 'phase') return 'reviewing_phase';
34
30
  if (state.current_review?.scope === 'task') return 'reviewing_task';
@@ -73,7 +69,8 @@ export async function init({ project, phases, research, force = false, basePath
73
69
  }
74
70
 
75
71
  const state = createInitialState({ project, phases });
76
- state.git_head = getGitHead(basePath);
72
+ if (state.error) return state;
73
+ state.git_head = await getGitHead(basePath);
77
74
 
78
75
  // Create plan.md placeholder (atomic write)
79
76
  await writeAtomic(
@@ -388,7 +385,7 @@ export async function phaseComplete({
388
385
 
389
386
  // Update git_head to current commit
390
387
  const gsdDir = dirname(statePath);
391
- state.git_head = getGitHead(dirname(gsdDir));
388
+ state.git_head = await getGitHead(dirname(gsdDir));
392
389
 
393
390
  // Prune evidence from old phases (in-memory to avoid double read/write)
394
391
  await _pruneEvidenceFromState(state, state.current_phase, gsdDir);
@@ -557,6 +554,12 @@ export function selectRunnableTask(phase, state, { maxRetry = DEFAULT_MAX_RETRY
557
554
  return { mode: 'trigger_review' };
558
555
  }
559
556
 
557
+ // All tasks accepted → trigger phase review if not already reviewed
558
+ const allAccepted = phase.todo.length > 0 && phase.todo.every(t => t.lifecycle === 'accepted');
559
+ if (allAccepted && phase.phase_review?.status !== 'accepted') {
560
+ return { mode: 'trigger_review' };
561
+ }
562
+
560
563
  const blockedTasks = phase.todo.filter(t => t.lifecycle === 'blocked');
561
564
  if (blockedTasks.length > 0) {
562
565
  return { mode: 'awaiting_user', blockers: blockedTasks.map(t => ({ id: t.id, reason: t.blocked_reason })) };
@@ -81,9 +81,10 @@ export async function runAll(cwd = process.cwd()) {
81
81
  const errResult = { exit_code: -1, summary: 'No package manager detected' };
82
82
  return { lint: errResult, typecheck: errResult, test: errResult };
83
83
  }
84
- return {
85
- lint: await runLint(pm, cwd),
86
- typecheck: await runTypeCheck(cwd),
87
- test: await runTests(pm, cwd),
88
- };
84
+ const [lint, typecheck, test] = await Promise.all([
85
+ runLint(pm, cwd),
86
+ runTypeCheck(cwd),
87
+ runTests(pm, cwd),
88
+ ]);
89
+ return { lint, typecheck, test };
89
90
  }
package/src/utils.js CHANGED
@@ -1,7 +1,10 @@
1
- import { readFile, writeFile, rename, mkdir } from 'node:fs/promises';
1
+ import { readFile, writeFile, rename, mkdir, unlink } from 'node:fs/promises';
2
2
  import { statSync } from 'node:fs';
3
3
  import { join, dirname, resolve } from 'node:path';
4
- import { execSync } from 'node:child_process';
4
+ import { execFile as execFileCb } from 'node:child_process';
5
+ import { promisify } from 'node:util';
6
+
7
+ const execFileAsync = promisify(execFileCb);
5
8
 
6
9
  export function getGsdDir(startDir = process.cwd()) {
7
10
  let dir = resolve(startDir);
@@ -23,18 +26,22 @@ export function getStatePath(startDir = process.cwd()) {
23
26
  return join(gsdDir, 'state.json');
24
27
  }
25
28
 
26
- export function getGitHead(cwd = process.cwd()) {
29
+ export async function getGitHead(cwd = process.cwd()) {
27
30
  try {
28
- return execSync('git rev-parse --short HEAD', {
31
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--short', 'HEAD'], {
29
32
  cwd,
30
- encoding: 'utf-8',
31
- stdio: ['ignore', 'pipe', 'ignore'],
32
- }).trim();
33
+ timeout: 5000,
34
+ });
35
+ return stdout.trim();
33
36
  } catch {
34
37
  return null;
35
38
  }
36
39
  }
37
40
 
41
+ export function isPlainObject(value) {
42
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
43
+ }
44
+
38
45
  export async function ensureDir(dirPath) {
39
46
  await mkdir(dirPath, { recursive: true });
40
47
  }
@@ -56,18 +63,28 @@ export async function readJson(filePath) {
56
63
  * Atomically write JSON data (write to .tmp then rename).
57
64
  */
58
65
  export async function writeJson(filePath, data) {
59
- const tmpPath = filePath + '.tmp';
66
+ const tmpPath = `${filePath}.${process.pid}-${Date.now()}.tmp`;
60
67
  await ensureDir(dirname(filePath));
61
- await writeFile(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
62
- await rename(tmpPath, filePath);
68
+ try {
69
+ await writeFile(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf-8');
70
+ await rename(tmpPath, filePath);
71
+ } catch (err) {
72
+ try { await unlink(tmpPath); } catch {}
73
+ throw err;
74
+ }
63
75
  }
64
76
 
65
77
  /**
66
78
  * Atomically write text content (write to .tmp then rename). [I-3]
67
79
  */
68
80
  export async function writeAtomic(filePath, content) {
69
- const tmpPath = filePath + '.tmp';
81
+ const tmpPath = `${filePath}.${process.pid}-${Date.now()}.tmp`;
70
82
  await ensureDir(dirname(filePath));
71
- await writeFile(tmpPath, content, 'utf-8');
72
- await rename(tmpPath, filePath);
83
+ try {
84
+ await writeFile(tmpPath, content, 'utf-8');
85
+ await rename(tmpPath, filePath);
86
+ } catch (err) {
87
+ try { await unlink(tmpPath); } catch {}
88
+ throw err;
89
+ }
73
90
  }
package/uninstall.js CHANGED
@@ -1,16 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  // Plugin uninstaller for GSD-Lite
3
3
 
4
- import { existsSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
4
+ import { existsSync, rmSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
5
5
  import { join } from 'node:path';
6
6
  import { homedir } from 'node:os';
7
7
  import { pathToFileURL } from 'node:url';
8
8
 
9
- const CLAUDE_DIR = join(homedir(), '.claude');
10
- const RUNTIME_DIR = join(CLAUDE_DIR, 'gsd-lite');
9
+ const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
10
+ const RUNTIME_DIR = join(CLAUDE_DIR, 'gsd');
11
11
 
12
12
  function log(msg) { console.log(msg); }
13
13
 
14
+ function atomicWriteSync(filePath, content) {
15
+ const tmp = filePath + `.${process.pid}-${Date.now()}.tmp`;
16
+ writeFileSync(tmp, content);
17
+ renameSync(tmp, filePath);
18
+ }
19
+
14
20
  function removeDir(path, label) {
15
21
  if (existsSync(path)) {
16
22
  rmSync(path, { recursive: true, force: true });
@@ -28,52 +34,108 @@ export function main() {
28
34
  removeDir(join(CLAUDE_DIR, 'agents', 'gsd'), 'agents/gsd/');
29
35
  removeDir(join(CLAUDE_DIR, 'workflows', 'gsd'), 'workflows/gsd/');
30
36
  removeDir(join(CLAUDE_DIR, 'references', 'gsd'), 'references/gsd/');
31
- removeDir(RUNTIME_DIR, 'gsd-lite runtime/');
37
+ removeDir(RUNTIME_DIR, 'gsd runtime/');
38
+ removeDir(join(CLAUDE_DIR, 'gsd-lite'), 'legacy gsd-lite runtime/');
39
+
40
+ // Remove hook files (both legacy and current names)
41
+ for (const name of ['context-monitor.js', 'gsd-statusline.cjs', 'gsd-context-monitor.cjs', 'gsd-session-init.cjs']) {
42
+ const hookFile = join(CLAUDE_DIR, 'hooks', name);
43
+ if (existsSync(hookFile)) {
44
+ rmSync(hookFile);
45
+ log(` ✓ Removed hooks/${name}`);
46
+ }
47
+ }
32
48
 
33
- // Remove hook file
34
- const hookFile = join(CLAUDE_DIR, 'hooks', 'context-monitor.js');
35
- if (existsSync(hookFile)) {
36
- rmSync(hookFile);
37
- log(' Removed hooks/context-monitor.js');
49
+ // Clean up plugin system directories (from /plugin install)
50
+ removeDir(join(CLAUDE_DIR, 'plugins', 'marketplaces', 'gsd'), 'plugins/marketplaces/gsd/');
51
+ removeDir(join(CLAUDE_DIR, 'plugins', 'cache', 'gsd'), 'plugins/cache/gsd/');
52
+ // Legacy "gsd-lite" plugin directories
53
+ removeDir(join(CLAUDE_DIR, 'plugins', 'marketplaces', 'gsd-lite'), 'plugins/marketplaces/gsd-lite/');
54
+ removeDir(join(CLAUDE_DIR, 'plugins', 'cache', 'gsd-lite'), 'plugins/cache/gsd-lite/');
55
+
56
+ // Deregister from plugin registry files
57
+ const pluginsDir = join(CLAUDE_DIR, 'plugins');
58
+ function removeJsonEntry(filePath, key, label) {
59
+ try {
60
+ const data = JSON.parse(readFileSync(filePath, 'utf-8'));
61
+ if (key in data) {
62
+ delete data[key];
63
+ atomicWriteSync(filePath, JSON.stringify(data, null, 2) + '\n');
64
+ log(` ✓ Removed '${key}' from ${label}`);
65
+ }
66
+ } catch {}
67
+ }
68
+ function removeNestedEntry(filePath, parentKey, key, label) {
69
+ try {
70
+ const data = JSON.parse(readFileSync(filePath, 'utf-8'));
71
+ if (data[parentKey] && key in data[parentKey]) {
72
+ delete data[parentKey][key];
73
+ atomicWriteSync(filePath, JSON.stringify(data, null, 2) + '\n');
74
+ log(` ✓ Removed '${key}' from ${label}`);
75
+ }
76
+ } catch {}
77
+ }
78
+ for (const name of ['gsd', 'gsd-lite']) {
79
+ removeJsonEntry(join(pluginsDir, 'known_marketplaces.json'), name, 'known_marketplaces.json');
80
+ removeNestedEntry(join(pluginsDir, 'installed_plugins.json'), 'plugins', `${name}@${name}`, 'installed_plugins.json');
38
81
  }
39
82
 
40
- // Deregister MCP server and hooks [M-10]
83
+ // Deregister MCP server, hooks, and plugin entries from settings.json
41
84
  const settingsPath = join(CLAUDE_DIR, 'settings.json');
42
85
  try {
43
86
  const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
44
87
  let changed = false;
45
- if (settings.mcpServers && settings.mcpServers['gsd-lite']) {
46
- delete settings.mcpServers['gsd-lite'];
47
- changed = true;
88
+ // Remove both current and legacy MCP server + plugin entries
89
+ for (const name of ['gsd', 'gsd-lite']) {
90
+ if (settings.mcpServers?.[name]) {
91
+ delete settings.mcpServers[name];
92
+ changed = true;
93
+ }
94
+ const pluginKey = `${name}@${name}`;
95
+ if (settings.enabledPlugins?.[pluginKey]) {
96
+ delete settings.enabledPlugins[pluginKey];
97
+ changed = true;
98
+ }
99
+ if (settings.extraKnownMarketplaces?.[name]) {
100
+ delete settings.extraKnownMarketplaces[name];
101
+ changed = true;
102
+ }
103
+ }
104
+ if (settings.extraKnownMarketplaces && Object.keys(settings.extraKnownMarketplaces).length === 0) {
105
+ delete settings.extraKnownMarketplaces;
48
106
  }
49
- // Remove top-level statusLine if GSD's
50
- if (settings.statusLine?.command?.includes('context-monitor.js')) {
107
+ // Remove top-level statusLine if GSD's (match both old and new patterns)
108
+ if (settings.statusLine?.command?.includes('gsd-statusline') ||
109
+ settings.statusLine?.command?.includes('context-monitor.js')) {
51
110
  delete settings.statusLine;
52
111
  changed = true;
53
112
  }
54
113
  if (settings.hooks) {
55
- // Remove legacy StatusLine string entry
114
+ // Remove legacy StatusLine hook entry
56
115
  if (typeof settings.hooks.StatusLine === 'string'
57
- && settings.hooks.StatusLine.includes('context-monitor.js')) {
116
+ && (settings.hooks.StatusLine.includes('gsd-statusline') ||
117
+ settings.hooks.StatusLine.includes('context-monitor.js'))) {
58
118
  delete settings.hooks.StatusLine;
59
119
  changed = true;
60
120
  }
61
- // Remove GSD PostToolUse entry from array
121
+ // Remove GSD PostToolUse entry from array (match both old and new patterns)
62
122
  if (Array.isArray(settings.hooks.PostToolUse)) {
63
123
  const len = settings.hooks.PostToolUse.length;
64
124
  settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(e =>
65
- !e.hooks?.some(h => h.command?.includes('context-monitor.js')));
125
+ !e.hooks?.some(h => h.command?.includes('gsd-context-monitor') ||
126
+ h.command?.includes('context-monitor.js')));
66
127
  if (settings.hooks.PostToolUse.length < len) changed = true;
67
128
  if (settings.hooks.PostToolUse.length === 0) delete settings.hooks.PostToolUse;
68
129
  } else if (typeof settings.hooks.PostToolUse === 'string'
69
- && settings.hooks.PostToolUse.includes('context-monitor.js')) {
130
+ && (settings.hooks.PostToolUse.includes('gsd-context-monitor') ||
131
+ settings.hooks.PostToolUse.includes('context-monitor.js'))) {
70
132
  delete settings.hooks.PostToolUse;
71
133
  changed = true;
72
134
  }
73
135
  }
74
136
  if (changed) {
75
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
76
- log(' ✓ MCP server + hooks deregistered from settings.json');
137
+ atomicWriteSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
138
+ log(' ✓ MCP server + hooks + plugin entries deregistered from settings.json');
77
139
  }
78
140
  } catch {}
79
141
 
@@ -1,7 +1,7 @@
1
1
  # 4 阶段根因分析
2
2
 
3
3
  > 本文档是 debugger 内联规则的扩展指南,按需加载。
4
- > 冲突时以 `gsd-debugger.md` 内联规则为准。
4
+ > 冲突时以 `debugger.md` 内联规则为准。
5
5
 
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  # 偏差处理规则
2
2
 
3
3
  > 本文档是 executor 内联 `<deviation_rules>` 的扩展指南,按需加载。
4
- > 冲突时以 `gsd-executor.md` 内联规则为准。
4
+ > 冲突时以 `executor.md` 内联规则为准。
5
5
 
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  # 研究工作流
2
2
 
3
3
  > 本文档是 researcher 内联规则的扩展指南,按需加载。
4
- > 冲突时以 `gsd-researcher.md` 内联规则为准。
4
+ > 冲突时以 `researcher.md` 内联规则为准。
5
5
 
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  # 分层审查循环
2
2
 
3
3
  > 本文档是 reviewer 内联审查策略的扩展指南,按需加载。
4
- > 冲突时以 `gsd-reviewer.md` 内联规则为准。
4
+ > 冲突时以 `reviewer.md` 内联规则为准。
5
5
 
6
6
  ---
7
7
 
@@ -1,7 +1,7 @@
1
1
  # TDD 循环: RED-GREEN-REFACTOR
2
2
 
3
3
  > 本文档是 executor 内联 TDD 铁律的扩展指南,按需加载。
4
- > 冲突时以 `gsd-executor.md` 内联规则为准。
4
+ > 冲突时以 `executor.md` 内联规则为准。
5
5
 
6
6
  ---
7
7