gsd-lite 0.3.15 → 0.3.17

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 (43) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.mcp.json +0 -0
  4. package/README.md +0 -0
  5. package/agents/debugger.md +0 -0
  6. package/agents/executor.md +0 -0
  7. package/agents/researcher.md +0 -0
  8. package/agents/reviewer.md +0 -0
  9. package/cli.js +5 -1
  10. package/commands/prd.md +0 -0
  11. package/commands/resume.md +0 -0
  12. package/commands/start.md +0 -0
  13. package/commands/status.md +0 -0
  14. package/commands/stop.md +0 -0
  15. package/hooks/context-monitor.js +0 -0
  16. package/hooks/gsd-auto-update.cjs +184 -60
  17. package/hooks/gsd-context-monitor.cjs +0 -0
  18. package/hooks/gsd-session-init.cjs +28 -10
  19. package/hooks/gsd-statusline.cjs +0 -0
  20. package/hooks/hooks.json +0 -0
  21. package/install.js +0 -0
  22. package/launcher.js +0 -0
  23. package/package.json +1 -1
  24. package/references/anti-rationalization-full.md +0 -0
  25. package/references/evidence-spec.md +0 -0
  26. package/references/execution-loop.md +0 -0
  27. package/references/git-worktrees.md +0 -0
  28. package/references/questioning.md +0 -0
  29. package/references/review-classification.md +0 -0
  30. package/references/state-diagram.md +0 -0
  31. package/references/testing-patterns.md +0 -0
  32. package/src/schema.js +0 -0
  33. package/src/server.js +0 -0
  34. package/src/tools/orchestrator.js +0 -0
  35. package/src/tools/state.js +0 -0
  36. package/src/tools/verify.js +0 -0
  37. package/src/utils.js +0 -0
  38. package/uninstall.js +0 -0
  39. package/workflows/debugging.md +0 -0
  40. package/workflows/deviation-rules.md +0 -0
  41. package/workflows/research.md +0 -0
  42. package/workflows/review-cycle.md +0 -0
  43. package/workflows/tdd-cycle.md +0 -0
@@ -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.3.15",
16
+ "version": "0.3.17",
17
17
  "keywords": [
18
18
  "orchestration",
19
19
  "mcp",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "description": "AI orchestration tool for Claude Code — GSD management shell + Superpowers quality core",
5
5
  "author": {
6
6
  "name": "sdsrss",
package/.mcp.json CHANGED
File without changes
package/README.md CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
package/cli.js CHANGED
@@ -40,7 +40,11 @@ switch (command) {
40
40
  if (result?.updated) {
41
41
  console.log(`\n✓ Updated: v${result.from} → v${result.to}`);
42
42
  } else if (result?.updateAvailable) {
43
- console.log(`\n! Update available v${result.to} but install failed. Try manually.`);
43
+ if (result.action === 'plugin_update') {
44
+ console.log(`\n! Update available: v${result.to}. Run /plugin update gsd`);
45
+ } else {
46
+ console.log(`\n! Update available v${result.to} but install failed. Try manually.`);
47
+ }
44
48
  } else if (!result) {
45
49
  console.log('✓ Already up to date');
46
50
  }
package/commands/prd.md CHANGED
File without changes
File without changes
package/commands/start.md CHANGED
File without changes
File without changes
package/commands/stop.md CHANGED
File without changes
File without changes
@@ -20,104 +20,200 @@ const claudeDir =
20
20
  const runtimeDir = path.join(claudeDir, 'gsd');
21
21
  const stateDir = path.join(runtimeDir, 'runtime');
22
22
  const STATE_FILE = path.join(stateDir, 'update-state.json');
23
+ const STATE_LOCK_FILE = path.join(stateDir, 'update-state.lock');
24
+ const NOTIFICATION_FILE = path.join(stateDir, 'update-notification.json');
23
25
  const pluginRoot = path.resolve(__dirname, '..');
24
26
 
27
+ const LOCK_STALE_MS = 10_000;
28
+ const LOCK_RETRY_MS = 50;
29
+ const LOCK_MAX_RETRIES = 100;
30
+
25
31
  // ── Main Entry ─────────────────────────────────────────────
26
32
  async function checkForUpdate(options = {}) {
27
- const { force = false, verbose = false, install = true } = options;
33
+ const {
34
+ force = false,
35
+ verbose = false,
36
+ install = true,
37
+ notify = false,
38
+ fetchLatestRelease: fetchLatestReleaseImpl = fetchLatestRelease,
39
+ downloadAndInstall: downloadAndInstallImpl = downloadAndInstall,
40
+ getCurrentVersion: getCurrentVersionImpl = getCurrentVersion,
41
+ } = options;
42
+ const installMode = getInstallMode();
28
43
 
29
44
  try {
30
45
  if (!force && shouldSkipUpdateCheck()) {
31
46
  if (verbose) console.log('Skipping update check (dev mode or auto-update in progress)');
32
47
  return null;
33
48
  }
34
-
35
- const state = readState();
36
- if (!force && !shouldCheck(state)) {
37
- if (state.updateAvailable && state.latestVersion) {
38
- return {
39
- updateAvailable: true,
40
- from: getCurrentVersion(),
41
- to: state.latestVersion,
42
- };
49
+ return await withFileLock(async () => {
50
+ const state = readState();
51
+ if (!force && !shouldCheck(state)) {
52
+ if (state.updateAvailable && state.latestVersion) {
53
+ return {
54
+ updateAvailable: true,
55
+ from: getCurrentVersionImpl(installMode),
56
+ to: state.latestVersion,
57
+ installMode,
58
+ };
59
+ }
60
+ if (verbose) console.log('Throttled — last check:', state.lastCheck);
61
+ return null;
43
62
  }
44
- if (verbose) console.log('Throttled — last check:', state.lastCheck);
45
- return null;
46
- }
47
63
 
48
- if (verbose) console.log('Checking GitHub for latest release...');
49
- const token = getGitHubToken();
50
- const latest = await fetchLatestRelease(token);
51
- if (!latest) {
52
- if (latest === false) state.rateLimited = true; // 403 rate-limited
53
- saveState({ ...state, lastCheck: new Date().toISOString() });
54
- if (verbose) console.log('Could not fetch latest release');
55
- return null;
56
- }
57
-
58
- // Successful fetch — clear rate-limit back-off
59
- state.rateLimited = false;
60
- const currentVersion = getCurrentVersion();
61
- if (verbose) console.log(`Current: v${currentVersion} — Latest: v${latest.version}`);
64
+ if (verbose) console.log('Checking GitHub for latest release...');
65
+ const token = getGitHubToken();
66
+ const latest = await fetchLatestReleaseImpl(token);
67
+ if (!latest) {
68
+ if (latest === false) state.rateLimited = true; // 403 rate-limited
69
+ saveState({ ...state, lastCheck: new Date().toISOString() });
70
+ if (verbose) console.log('Could not fetch latest release');
71
+ return null;
72
+ }
62
73
 
63
- const hasUpdate = compareVersions(latest.version, currentVersion) > 0;
74
+ // Successful fetch clear rate-limit back-off
75
+ state.rateLimited = false;
76
+ const currentVersion = getCurrentVersionImpl(installMode);
77
+ if (verbose) console.log(`Current: v${currentVersion} — Latest: v${latest.version}`);
78
+
79
+ const hasUpdate = compareVersions(latest.version, currentVersion) > 0;
80
+
81
+ if (hasUpdate) {
82
+ if (installMode === 'plugin' && install) {
83
+ saveState({
84
+ ...state,
85
+ lastCheck: new Date().toISOString(),
86
+ latestVersion: latest.version,
87
+ updateAvailable: true,
88
+ });
89
+ if (notify) {
90
+ writeNotification({
91
+ kind: 'available',
92
+ from: currentVersion,
93
+ to: latest.version,
94
+ action: 'plugin_update',
95
+ });
96
+ }
97
+ return {
98
+ updateAvailable: true,
99
+ from: currentVersion,
100
+ to: latest.version,
101
+ action: 'plugin_update',
102
+ autoInstallSupported: false,
103
+ installMode,
104
+ };
105
+ }
106
+
107
+ if (!install) {
108
+ // Check-only mode (used by SessionStart hook)
109
+ saveState({
110
+ ...state,
111
+ lastCheck: new Date().toISOString(),
112
+ latestVersion: latest.version,
113
+ updateAvailable: true,
114
+ });
115
+ return {
116
+ updateAvailable: true,
117
+ from: currentVersion,
118
+ to: latest.version,
119
+ installMode,
120
+ };
121
+ }
122
+
123
+ if (verbose) console.log(`Downloading v${latest.version}...`);
124
+ const success = await downloadAndInstallImpl(latest.tarballUrl, verbose, token);
64
125
 
65
- if (hasUpdate) {
66
- if (!install) {
67
- // Check-only mode (used by SessionStart hook)
68
126
  saveState({
69
127
  ...state,
70
128
  lastCheck: new Date().toISOString(),
71
129
  latestVersion: latest.version,
72
- updateAvailable: true,
130
+ updateAvailable: !success,
131
+ lastUpdate: success ? new Date().toISOString() : state.lastUpdate,
73
132
  });
133
+
134
+ if (success && notify) {
135
+ writeNotification({
136
+ kind: 'updated',
137
+ from: currentVersion,
138
+ to: latest.version,
139
+ });
140
+ }
141
+
74
142
  return {
75
- updateAvailable: true,
143
+ updateAvailable: !success,
144
+ updated: success,
76
145
  from: currentVersion,
77
146
  to: latest.version,
147
+ installMode,
78
148
  };
79
149
  }
80
150
 
81
- if (verbose) console.log(`Downloading v${latest.version}...`);
82
- const success = await downloadAndInstall(latest.tarballUrl, verbose, token);
83
-
151
+ // No update needed
84
152
  saveState({
85
153
  ...state,
86
154
  lastCheck: new Date().toISOString(),
87
155
  latestVersion: latest.version,
88
- updateAvailable: !success,
89
- lastUpdate: success ? new Date().toISOString() : state.lastUpdate,
156
+ updateAvailable: false,
90
157
  });
91
-
92
- return {
93
- updateAvailable: !success,
94
- updated: success,
95
- from: currentVersion,
96
- to: latest.version,
97
- };
98
- }
99
-
100
- // No update needed
101
- saveState({
102
- ...state,
103
- lastCheck: new Date().toISOString(),
104
- latestVersion: latest.version,
105
- updateAvailable: false,
158
+ if (verbose) console.log('Already up to date');
159
+ return null;
106
160
  });
107
- if (verbose) console.log('Already up to date');
108
- return null;
109
161
  } catch (err) {
110
162
  if (verbose) console.error('Update check failed:', err.message);
111
163
  return null;
112
164
  }
113
165
  }
114
166
 
167
+ async function withFileLock(fn) {
168
+ let acquired = false;
169
+ try {
170
+ fs.mkdirSync(stateDir, { recursive: true });
171
+ } catch {
172
+ /* best effort */
173
+ }
174
+
175
+ for (let i = 0; i < LOCK_MAX_RETRIES; i++) {
176
+ try {
177
+ fs.writeFileSync(STATE_LOCK_FILE, String(process.pid), { flag: 'wx' });
178
+ acquired = true;
179
+ break;
180
+ } catch (err) {
181
+ if (err.code === 'EEXIST') {
182
+ try {
183
+ const stats = fs.statSync(STATE_LOCK_FILE);
184
+ if (Date.now() - stats.mtimeMs > LOCK_STALE_MS) {
185
+ try { fs.rmSync(STATE_LOCK_FILE, { force: true }); } catch {}
186
+ continue;
187
+ }
188
+ } catch {
189
+ continue;
190
+ }
191
+ await new Promise(resolve => setTimeout(resolve, LOCK_RETRY_MS));
192
+ } else {
193
+ break;
194
+ }
195
+ }
196
+ }
197
+
198
+ try {
199
+ return await fn();
200
+ } finally {
201
+ if (acquired) {
202
+ try { fs.rmSync(STATE_LOCK_FILE, { force: true }); } catch {}
203
+ }
204
+ }
205
+ }
206
+
115
207
  // ── Skip Check Detection ──────────────────────────────────
116
208
  // Returns true when update checks should be skipped:
117
209
  // 1. PLUGIN_AUTO_UPDATE env set → recursive guard (auto-update already in progress)
118
210
  // 2. Running from a git clone → dev mode (developer working on source)
119
211
  function shouldSkipUpdateCheck() {
120
212
  if (process.env.PLUGIN_AUTO_UPDATE) return true;
213
+ return isDevMode();
214
+ }
215
+
216
+ function isDevMode() {
121
217
  try {
122
218
  if (!fs.existsSync(path.join(pluginRoot, '.git'))) return false;
123
219
  const pkg = JSON.parse(
@@ -129,6 +225,13 @@ function shouldSkipUpdateCheck() {
129
225
  }
130
226
  }
131
227
 
228
+ function getInstallMode() {
229
+ if (isDevMode()) return 'dev';
230
+ return path.resolve(pluginRoot) === path.resolve(claudeDir)
231
+ ? 'manual'
232
+ : 'plugin';
233
+ }
234
+
132
235
  // ── Throttle ───────────────────────────────────────────────
133
236
  function shouldCheck(state) {
134
237
  if (!state.lastCheck) return true;
@@ -196,11 +299,11 @@ function compareVersions(a, b) {
196
299
  return 0;
197
300
  }
198
301
 
199
- function getCurrentVersion() {
200
- for (const p of [
201
- path.join(pluginRoot, 'package.json'),
202
- path.join(runtimeDir, 'package.json'),
203
- ]) {
302
+ function getCurrentVersion(mode = getInstallMode()) {
303
+ const candidates = mode === 'manual'
304
+ ? [path.join(runtimeDir, 'package.json'), path.join(pluginRoot, 'package.json')]
305
+ : [path.join(pluginRoot, 'package.json'), path.join(runtimeDir, 'package.json')];
306
+ for (const p of candidates) {
204
307
  try {
205
308
  return JSON.parse(fs.readFileSync(p, 'utf8')).version;
206
309
  } catch {
@@ -280,9 +383,30 @@ function saveState(state) {
280
383
  }
281
384
  }
282
385
 
386
+ function writeNotification(notification) {
387
+ try {
388
+ fs.mkdirSync(stateDir, { recursive: true });
389
+ const tmpPath = NOTIFICATION_FILE + `.${process.pid}.tmp`;
390
+ fs.writeFileSync(tmpPath, JSON.stringify(notification, null, 2) + '\n');
391
+ fs.renameSync(tmpPath, NOTIFICATION_FILE);
392
+ } catch {
393
+ /* silent */
394
+ }
395
+ }
396
+
283
397
  module.exports = {
284
398
  checkForUpdate,
285
399
  getCurrentVersion,
286
400
  compareVersions,
401
+ getInstallMode,
402
+ isDevMode,
403
+ shouldCheck,
287
404
  shouldSkipUpdateCheck,
288
405
  };
406
+
407
+ // ── CLI Entry Point (for background auto-install) ─────────
408
+ if (require.main === module) {
409
+ checkForUpdate({ install: true, verbose: false, notify: true })
410
+ .catch(() => {})
411
+ .finally(() => process.exit(0));
412
+ }
File without changes
@@ -1,7 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  // GSD-Lite SessionStart hook
3
- // 1. Auto-registers statusLine in settings.json if not already configured.
4
- // 2. Checks for updates and notifies user (no download in hook context).
3
+ // 1. Cleans up stale temp files (throttled to once/day).
4
+ // 2. Auto-registers statusLine in settings.json if not already configured.
5
+ // 3. Shows notification if a previous background update completed or found a new version.
6
+ // 4. Spawns background auto-update (detached, non-blocking).
5
7
  // Idempotent: skips if statusLine already points to gsd-statusline, preserves
6
8
  // third-party statuslines.
7
9
 
@@ -64,15 +66,31 @@ setTimeout(() => process.exit(0), 4000).unref();
64
66
  }
65
67
  } catch { /* silent */ }
66
68
 
67
- // ── Phase 3: Auto-update check (notify only, no download) ──
68
- // checkForUpdate handles throttling internally via shouldCheck()
69
+ // ── Phase 3: Show notification from previous background auto-install ──
69
70
  try {
70
- const { checkForUpdate } = require('./gsd-auto-update.cjs');
71
- const result = await checkForUpdate({ install: false });
72
- if (result?.updateAvailable) {
73
- console.log(
74
- `\n📦 GSD-Lite v${result.to} available (current: v${result.from}). Run: gsd update`
75
- );
71
+ const notifPath = path.join(claudeDir, 'gsd', 'runtime', 'update-notification.json');
72
+ if (fs.existsSync(notifPath)) {
73
+ const notif = JSON.parse(fs.readFileSync(notifPath, 'utf8'));
74
+ if (notif.kind === 'updated') {
75
+ console.log(`✅ GSD-Lite auto-updated: v${notif.from} v${notif.to}`);
76
+ } else if (notif.kind === 'available' && notif.action === 'plugin_update') {
77
+ console.log(`📦 GSD-Lite update available: v${notif.from} → v${notif.to}. Run /plugin update gsd`);
78
+ } else if (notif.kind === 'available') {
79
+ console.log(`📦 GSD-Lite update available: v${notif.from} → v${notif.to}. Run gsd update`);
80
+ }
81
+ fs.unlinkSync(notifPath);
76
82
  }
83
+ } catch { /* silent */ }
84
+
85
+ // ── Phase 4: Spawn background auto-update (non-blocking) ──
86
+ // Detached child handles check + download + install; throttled by shouldCheck()
87
+ try {
88
+ const { spawn } = require('node:child_process');
89
+ const child = spawn(
90
+ process.execPath,
91
+ [path.join(__dirname, 'gsd-auto-update.cjs')],
92
+ { detached: true, stdio: 'ignore' },
93
+ );
94
+ child.unref();
77
95
  } catch { /* silent — never block session start */ }
78
96
  })().catch(() => {});
File without changes
package/hooks/hooks.json CHANGED
File without changes
package/install.js CHANGED
File without changes
package/launcher.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-lite",
3
- "version": "0.3.15",
3
+ "version": "0.3.17",
4
4
  "description": "AI orchestration tool for Claude Code — GSD management shell + Superpowers quality core",
5
5
  "type": "module",
6
6
  "bin": {
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/src/schema.js CHANGED
File without changes
package/src/server.js CHANGED
File without changes
File without changes
File without changes
File without changes
package/src/utils.js CHANGED
File without changes
package/uninstall.js CHANGED
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes