gsd-lite 0.3.13 → 0.3.15

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 +2 -2
  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 +15 -0
  10. package/commands/prd.md +1 -1
  11. package/commands/resume.md +0 -0
  12. package/commands/start.md +1 -1
  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 +288 -0
  17. package/hooks/gsd-context-monitor.cjs +0 -0
  18. package/hooks/gsd-session-init.cjs +62 -45
  19. package/hooks/gsd-statusline.cjs +0 -0
  20. package/hooks/hooks.json +1 -1
  21. package/install.js +37 -21
  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 +7 -1
  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 +18 -14
  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.13",
16
+ "version": "0.3.15",
17
17
  "keywords": [
18
18
  "orchestration",
19
19
  "mcp",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd",
3
- "version": "0.3.13",
3
+ "version": "0.3.15",
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
@@ -228,7 +228,7 @@ gsd-lite/
228
228
  ├── workflows/ # 5 core workflows (TDD, review, debug, research, deviation)
229
229
  ├── references/ # 8 reference docs (execution loop, state diagram, evidence spec, ...)
230
230
  ├── hooks/ # Context monitoring (StatusLine + PostToolUse)
231
- ├── tests/ # 575 tests (506 unit + 69 E2E)
231
+ ├── tests/ # 674 tests (unit + simulation + E2E)
232
232
  ├── cli.js # Install/uninstall CLI entry
233
233
  ├── install.js # Installation script
234
234
  └── uninstall.js # Uninstall script
@@ -237,7 +237,7 @@ gsd-lite/
237
237
  ## Testing
238
238
 
239
239
  ```bash
240
- npm test # Run all 575 tests
240
+ npm test # Run all 674 tests
241
241
  npm run test:coverage # Tests + coverage report (94%+ lines, 81%+ branches)
242
242
  npm run lint # Biome lint
243
243
  node --test tests/file.js # Run a single test file
File without changes
File without changes
File without changes
File without changes
package/cli.js CHANGED
@@ -8,6 +8,7 @@ Usage:
8
8
  gsd serve # Start MCP stdio server (explicit)
9
9
  gsd install # Install hooks/commands into Claude Code
10
10
  gsd uninstall # Remove hooks/commands from Claude Code
11
+ gsd update # Check for updates and install if available
11
12
  gsd help # Show this help
12
13
  `);
13
14
  }
@@ -31,6 +32,20 @@ switch (command) {
31
32
  uninstall();
32
33
  break;
33
34
  }
35
+ case 'update': {
36
+ const { checkForUpdate } = await import('./hooks/gsd-auto-update.cjs');
37
+ const force = process.argv.includes('--force');
38
+ console.log('Checking for updates...');
39
+ const result = await checkForUpdate({ force, verbose: true, install: true });
40
+ if (result?.updated) {
41
+ console.log(`\n✓ Updated: v${result.from} → v${result.to}`);
42
+ } else if (result?.updateAvailable) {
43
+ console.log(`\n! Update available v${result.to} but install failed. Try manually.`);
44
+ } else if (!result) {
45
+ console.log('✓ Already up to date');
46
+ }
47
+ break;
48
+ }
34
49
  case 'help':
35
50
  case '--help':
36
51
  case '-h':
package/commands/prd.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Start project from requirements document or description text
2
+ description: "Start project from requirements document or description text. Use when: (1) user provides a requirements doc, PRD, spec file, or design document, (2) user pastes a detailed feature description or task list, (3) user says '按这个需求做' or 'here\\'s what I need built', (4) user has a clear written specification ready to plan and execute"
3
3
  argument-hint: File path to requirements doc, or inline description text
4
4
  ---
5
5
 
File without changes
package/commands/start.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: Interactive project start — discuss requirements, research, plan, then auto-execute
2
+ description: "Interactive project start — discuss requirements, research, plan, then auto-execute. Use when: (1) user wants to implement a new feature or complex functionality, (2) user wants to research/analyze a technology then build something, (3) user describes a multi-step task that needs planning and decomposition, (4) user says '帮我做/实现/开发/搞一个...' or 'let\\'s build/create/implement...', (5) task is too complex for a single-shot response and needs phased execution"
3
3
  argument-hint: Optional feature or project description
4
4
  ---
5
5
 
File without changes
package/commands/stop.md CHANGED
File without changes
File without changes
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ // GSD-Lite Auto-Update Module
3
+ // Checks GitHub Releases for new versions and auto-installs updates.
4
+ // CJS format to match other hook modules.
5
+
6
+ const fs = require('node:fs');
7
+ const path = require('node:path');
8
+ const os = require('node:os');
9
+ const { execSync, spawnSync } = require('node:child_process');
10
+
11
+ // ── Configuration ──────────────────────────────────────────
12
+ const GITHUB_REPO = 'sdsrss/gsd-lite';
13
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
14
+ const RATE_LIMIT_INTERVAL_MS = 48 * 60 * 60 * 1000; // 48h back-off if rate-limited
15
+ const FETCH_TIMEOUT_MS = 3000; // 3s network timeout
16
+
17
+ // ── Paths ──────────────────────────────────────────────────
18
+ const claudeDir =
19
+ process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
20
+ const runtimeDir = path.join(claudeDir, 'gsd');
21
+ const stateDir = path.join(runtimeDir, 'runtime');
22
+ const STATE_FILE = path.join(stateDir, 'update-state.json');
23
+ const pluginRoot = path.resolve(__dirname, '..');
24
+
25
+ // ── Main Entry ─────────────────────────────────────────────
26
+ async function checkForUpdate(options = {}) {
27
+ const { force = false, verbose = false, install = true } = options;
28
+
29
+ try {
30
+ if (!force && shouldSkipUpdateCheck()) {
31
+ if (verbose) console.log('Skipping update check (dev mode or auto-update in progress)');
32
+ return null;
33
+ }
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
+ };
43
+ }
44
+ if (verbose) console.log('Throttled — last check:', state.lastCheck);
45
+ return null;
46
+ }
47
+
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}`);
62
+
63
+ const hasUpdate = compareVersions(latest.version, currentVersion) > 0;
64
+
65
+ if (hasUpdate) {
66
+ if (!install) {
67
+ // Check-only mode (used by SessionStart hook)
68
+ saveState({
69
+ ...state,
70
+ lastCheck: new Date().toISOString(),
71
+ latestVersion: latest.version,
72
+ updateAvailable: true,
73
+ });
74
+ return {
75
+ updateAvailable: true,
76
+ from: currentVersion,
77
+ to: latest.version,
78
+ };
79
+ }
80
+
81
+ if (verbose) console.log(`Downloading v${latest.version}...`);
82
+ const success = await downloadAndInstall(latest.tarballUrl, verbose, token);
83
+
84
+ saveState({
85
+ ...state,
86
+ lastCheck: new Date().toISOString(),
87
+ latestVersion: latest.version,
88
+ updateAvailable: !success,
89
+ lastUpdate: success ? new Date().toISOString() : state.lastUpdate,
90
+ });
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,
106
+ });
107
+ if (verbose) console.log('Already up to date');
108
+ return null;
109
+ } catch (err) {
110
+ if (verbose) console.error('Update check failed:', err.message);
111
+ return null;
112
+ }
113
+ }
114
+
115
+ // ── Skip Check Detection ──────────────────────────────────
116
+ // Returns true when update checks should be skipped:
117
+ // 1. PLUGIN_AUTO_UPDATE env set → recursive guard (auto-update already in progress)
118
+ // 2. Running from a git clone → dev mode (developer working on source)
119
+ function shouldSkipUpdateCheck() {
120
+ if (process.env.PLUGIN_AUTO_UPDATE) return true;
121
+ try {
122
+ if (!fs.existsSync(path.join(pluginRoot, '.git'))) return false;
123
+ const pkg = JSON.parse(
124
+ fs.readFileSync(path.join(pluginRoot, 'package.json'), 'utf8'),
125
+ );
126
+ return pkg.name === 'gsd-lite';
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+
132
+ // ── Throttle ───────────────────────────────────────────────
133
+ function shouldCheck(state) {
134
+ if (!state.lastCheck) return true;
135
+ const elapsed = Date.now() - new Date(state.lastCheck).getTime();
136
+ const interval = state.rateLimited
137
+ ? RATE_LIMIT_INTERVAL_MS
138
+ : CHECK_INTERVAL_MS;
139
+ return elapsed >= interval;
140
+ }
141
+
142
+ // ── GitHub Auth ─────────────────────────────────────────────
143
+ function getGitHubToken() {
144
+ // Try gh CLI token first (5000 req/hour vs 60 unauthenticated)
145
+ try {
146
+ return execSync('gh auth token', { timeout: 2000, stdio: ['pipe', 'pipe', 'pipe'] })
147
+ .toString()
148
+ .trim();
149
+ } catch {
150
+ return null;
151
+ }
152
+ }
153
+
154
+ // ── GitHub API ─────────────────────────────────────────────
155
+ async function fetchLatestRelease(token) {
156
+ const url = `https://api.github.com/repos/${GITHUB_REPO}/releases/latest`;
157
+ const controller = new AbortController();
158
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
159
+
160
+ try {
161
+ const headers = {
162
+ Accept: 'application/vnd.github+json',
163
+ 'User-Agent': 'gsd-lite-auto-update/1.0',
164
+ };
165
+ if (token) headers.Authorization = `Bearer ${token}`;
166
+
167
+ const res = await fetch(url, {
168
+ signal: controller.signal,
169
+ headers,
170
+ });
171
+
172
+ if (res.status === 403) return false; // rate-limited — caller sets flag
173
+ if (!res.ok) return null;
174
+
175
+ const data = await res.json();
176
+ return {
177
+ version: data.tag_name.replace(/^v/, ''),
178
+ tarballUrl: data.tarball_url,
179
+ releaseUrl: data.html_url,
180
+ };
181
+ } catch {
182
+ return null;
183
+ } finally {
184
+ clearTimeout(timeout);
185
+ }
186
+ }
187
+
188
+ // ── Version Comparison (semver) ────────────────────────────
189
+ function compareVersions(a, b) {
190
+ const pa = a.split('.').map(Number);
191
+ const pb = b.split('.').map(Number);
192
+ for (let i = 0; i < 3; i++) {
193
+ if ((pa[i] || 0) > (pb[i] || 0)) return 1;
194
+ if ((pa[i] || 0) < (pb[i] || 0)) return -1;
195
+ }
196
+ return 0;
197
+ }
198
+
199
+ function getCurrentVersion() {
200
+ for (const p of [
201
+ path.join(pluginRoot, 'package.json'),
202
+ path.join(runtimeDir, 'package.json'),
203
+ ]) {
204
+ try {
205
+ return JSON.parse(fs.readFileSync(p, 'utf8')).version;
206
+ } catch {
207
+ /* try next */
208
+ }
209
+ }
210
+ return '0.0.0';
211
+ }
212
+
213
+ // ── Download & Install ─────────────────────────────────────
214
+ async function downloadAndInstall(tarballUrl, verbose = false, token = null) {
215
+ const tmpDir = path.join(os.tmpdir(), `gsd-update-${Date.now()}`);
216
+ try {
217
+ fs.mkdirSync(tmpDir, { recursive: true });
218
+
219
+ // Download tarball via fetch (no shell interpolation)
220
+ if (verbose) console.log(' Downloading tarball...');
221
+ const headers = { Accept: 'application/vnd.github+json', 'User-Agent': 'gsd-lite-auto-update/1.0' };
222
+ if (token) headers.Authorization = `Bearer ${token}`;
223
+
224
+ const controller = new AbortController();
225
+ const dlTimeout = setTimeout(() => controller.abort(), 30000);
226
+ let tarData;
227
+ try {
228
+ const res = await fetch(tarballUrl, { signal: controller.signal, headers, redirect: 'follow' });
229
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
230
+ tarData = Buffer.from(await res.arrayBuffer());
231
+ } finally {
232
+ clearTimeout(dlTimeout);
233
+ }
234
+
235
+ // Write tarball to file, then extract with spawnSync (no shell)
236
+ const tarPath = path.join(tmpDir, 'release.tar.gz');
237
+ fs.writeFileSync(tarPath, tarData);
238
+ const tar = spawnSync('tar', ['xzf', tarPath, '-C', tmpDir, '--strip-components=1'], { timeout: 30000 });
239
+ if (tar.status !== 0) throw new Error(`tar extract failed: ${(tar.stderr || '').toString().slice(0, 200)}`);
240
+
241
+ // Run installer with spawnSync (no shell)
242
+ if (verbose) console.log(' Running installer...');
243
+ const install = spawnSync(process.execPath, [path.join(tmpDir, 'install.js')], {
244
+ timeout: 60000,
245
+ stdio: verbose ? 'inherit' : 'pipe',
246
+ env: { ...process.env, PLUGIN_AUTO_UPDATE: '1' },
247
+ });
248
+ if (install.status !== 0) throw new Error(`Installer failed: ${(install.stderr || '').toString().slice(0, 200)}`);
249
+
250
+ return true;
251
+ } catch (err) {
252
+ if (verbose) console.error(' Install failed:', err.message);
253
+ return false;
254
+ } finally {
255
+ try {
256
+ fs.rmSync(tmpDir, { recursive: true, force: true });
257
+ } catch {
258
+ /* ignore */
259
+ }
260
+ }
261
+ }
262
+
263
+ // ── State Persistence ──────────────────────────────────────
264
+ function readState() {
265
+ try {
266
+ return JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
267
+ } catch {
268
+ return {};
269
+ }
270
+ }
271
+
272
+ function saveState(state) {
273
+ try {
274
+ fs.mkdirSync(stateDir, { recursive: true });
275
+ const tmpPath = STATE_FILE + `.${process.pid}.tmp`;
276
+ fs.writeFileSync(tmpPath, JSON.stringify(state, null, 2) + '\n');
277
+ fs.renameSync(tmpPath, STATE_FILE);
278
+ } catch {
279
+ /* silent */
280
+ }
281
+ }
282
+
283
+ module.exports = {
284
+ checkForUpdate,
285
+ getCurrentVersion,
286
+ compareVersions,
287
+ shouldSkipUpdateCheck,
288
+ };
File without changes
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // GSD-Lite SessionStart hook
3
- // Auto-registers statusLine in settings.json if not already configured.
4
- // This bridges the gap for plugin marketplace installs (which don't run install.js).
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).
5
5
  // Idempotent: skips if statusLine already points to gsd-statusline, preserves
6
6
  // third-party statuslines.
7
7
 
@@ -14,48 +14,65 @@ const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.cla
14
14
  const settingsPath = path.join(claudeDir, 'settings.json');
15
15
  const statuslineScript = path.join(pluginRoot, 'hooks', 'gsd-statusline.cjs');
16
16
 
17
- // Clean up stale bridge/debounce files from previous sessions (older than 24h)
18
- try {
19
- const tmpDir = os.tmpdir();
20
- const DAY_MS = 24 * 60 * 60 * 1000;
21
- const now = Date.now();
22
- for (const entry of fs.readdirSync(tmpDir)) {
23
- if (!entry.startsWith('gsd-ctx-')) continue;
17
+ // Safety: exit after 4s regardless (hook timeout is 5s)
18
+ setTimeout(() => process.exit(0), 4000).unref();
19
+
20
+ (async () => {
21
+ // ── Phase 1: Clean up stale bridge/debounce files (throttled to once/day) ──
22
+ try {
23
+ const cleanupMarker = path.join(claudeDir, 'gsd', 'runtime', 'last-cleanup');
24
+ const DAY_MS = 24 * 60 * 60 * 1000;
25
+ const now = Date.now();
26
+ let shouldClean = true;
24
27
  try {
25
- const fullPath = path.join(tmpDir, entry);
26
- if (now - fs.statSync(fullPath).mtimeMs > DAY_MS) fs.unlinkSync(fullPath);
27
- } catch { /* skip */ }
28
- }
29
- } catch { /* silent */ }
30
-
31
- try {
32
- // Verify the statusline script exists (sanity check)
33
- if (!fs.existsSync(statuslineScript)) {
34
- process.exit(0);
35
- }
36
-
37
- let settings = {};
28
+ shouldClean = now - fs.statSync(cleanupMarker).mtimeMs > DAY_MS;
29
+ } catch { /* no marker = first run */ }
30
+
31
+ if (shouldClean) {
32
+ const tmpDir = os.tmpdir();
33
+ for (const entry of fs.readdirSync(tmpDir)) {
34
+ if (!entry.startsWith('gsd-ctx-')) continue;
35
+ try {
36
+ const fullPath = path.join(tmpDir, entry);
37
+ if (now - fs.statSync(fullPath).mtimeMs > DAY_MS) fs.unlinkSync(fullPath);
38
+ } catch { /* skip */ }
39
+ }
40
+ try {
41
+ fs.mkdirSync(path.dirname(cleanupMarker), { recursive: true });
42
+ fs.writeFileSync(cleanupMarker, String(now));
43
+ } catch { /* skip */ }
44
+ }
45
+ } catch { /* silent */ }
46
+
47
+ // ── Phase 2: StatusLine auto-registration ──
48
+ try {
49
+ if (fs.existsSync(statuslineScript)) {
50
+ let settings = {};
51
+ try {
52
+ settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
53
+ } catch { /* Can't read settings — skip registration */ }
54
+
55
+ if (settings && !settings.statusLine?.command) {
56
+ settings.statusLine = {
57
+ type: 'command',
58
+ command: `node ${JSON.stringify(statuslineScript)}`
59
+ };
60
+ const tmpPath = settingsPath + `.gsd-tmp-${process.pid}`;
61
+ fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n');
62
+ fs.renameSync(tmpPath, settingsPath);
63
+ }
64
+ }
65
+ } catch { /* silent */ }
66
+
67
+ // ── Phase 3: Auto-update check (notify only, no download) ──
68
+ // checkForUpdate handles throttling internally via shouldCheck()
38
69
  try {
39
- settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
40
- } catch {
41
- process.exit(0); // Can't read settings — don't risk writing a broken file
42
- }
43
-
44
- // Already has a statusLine configured (ours or third-party) — don't overwrite
45
- if (settings.statusLine?.command) {
46
- process.exit(0);
47
- }
48
-
49
- // Register our statusLine
50
- settings.statusLine = {
51
- type: 'command',
52
- command: `node ${JSON.stringify(statuslineScript)}`
53
- };
54
-
55
- // Atomic write to avoid corruption
56
- const tmpPath = settingsPath + `.gsd-tmp-${process.pid}`;
57
- fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n');
58
- fs.renameSync(tmpPath, settingsPath);
59
- } catch {
60
- // Silent fail — never block session start
61
- }
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
+ );
76
+ }
77
+ } catch { /* silent — never block session start */ }
78
+ })().catch(() => {});
File without changes
package/hooks/hooks.json CHANGED
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "type": "command",
10
10
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-session-init.cjs\"",
11
- "timeout": 3
11
+ "timeout": 5
12
12
  }
13
13
  ]
14
14
  }
package/install.js CHANGED
@@ -12,8 +12,16 @@ const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
12
12
  const RUNTIME_DIR = join(CLAUDE_DIR, 'gsd');
13
13
  const DRY_RUN = process.argv.includes('--dry-run');
14
14
 
15
- function log(msg) { console.log(msg); }
15
+ // Single source of truth for hook files (used by copy loop and registration)
16
+ const HOOK_FILES = ['gsd-session-init.cjs', 'gsd-auto-update.cjs', 'gsd-context-monitor.cjs', 'gsd-statusline.cjs'];
17
+
18
+ // Hook registration config: hookType → { file identifier, matcher, timeout? }
19
+ const HOOK_REGISTRY = [
20
+ { hookType: 'SessionStart', identifier: 'gsd-session-init', matcher: 'startup', timeout: 5 },
21
+ { hookType: 'PostToolUse', identifier: 'gsd-context-monitor', matcher: '*' },
22
+ ];
16
23
 
24
+ function log(msg) { console.log(msg); }
17
25
 
18
26
  function registerStatusLine(settings, statuslineScriptPath) {
19
27
  const command = `node ${JSON.stringify(statuslineScriptPath)}`;
@@ -29,27 +37,31 @@ function registerStatusLine(settings, statuslineScriptPath) {
29
37
  return true;
30
38
  }
31
39
 
32
- function registerPostToolUseHook(hooks, contextMonitorPath) {
33
- const command = `node ${JSON.stringify(contextMonitorPath)}`;
34
- const entry = { matcher: '*', hooks: [{ type: 'command', command }] };
35
- if (!hooks.PostToolUse) {
36
- hooks.PostToolUse = [entry];
40
+ function registerHookEntry(hooks, { hookType, identifier, matcher, timeout }) {
41
+ const scriptPath = join(CLAUDE_DIR, 'hooks', `${identifier}.cjs`);
42
+ const command = `node ${JSON.stringify(scriptPath)}`;
43
+ const hookDef = { type: 'command', command };
44
+ if (timeout) hookDef.timeout = timeout;
45
+ const entry = { matcher, hooks: [hookDef] };
46
+
47
+ if (!hooks[hookType]) {
48
+ hooks[hookType] = [entry];
37
49
  return true;
38
50
  }
39
51
  // Handle legacy string format
40
- if (typeof hooks.PostToolUse === 'string') {
41
- if (!hooks.PostToolUse.includes('gsd-context-monitor')) {
42
- log(' ! Preserved existing PostToolUse hook');
52
+ if (typeof hooks[hookType] === 'string') {
53
+ if (!hooks[hookType].includes(identifier)) {
54
+ log(` ! Preserved existing ${hookType} hook`);
43
55
  return false;
44
56
  }
45
- hooks.PostToolUse = [entry];
57
+ hooks[hookType] = [entry];
46
58
  return true;
47
59
  }
48
- if (Array.isArray(hooks.PostToolUse)) {
49
- const idx = hooks.PostToolUse.findIndex(e =>
50
- e.hooks?.some(h => h.command?.includes('gsd-context-monitor')));
51
- if (idx >= 0) hooks.PostToolUse[idx] = entry;
52
- else hooks.PostToolUse.push(entry);
60
+ if (Array.isArray(hooks[hookType])) {
61
+ const idx = hooks[hookType].findIndex(e =>
62
+ e.hooks?.some(h => h.command?.includes(identifier)));
63
+ if (idx >= 0) hooks[hookType][idx] = entry;
64
+ else hooks[hookType].push(entry);
53
65
  return true;
54
66
  }
55
67
  return false;
@@ -109,8 +121,10 @@ export function main() {
109
121
  // 4. References
110
122
  copyDir(join(__dirname, 'references'), join(CLAUDE_DIR, 'references', 'gsd'), 'references → ~/.claude/references/gsd/');
111
123
 
112
- // 5. Hooks
113
- copyDir(join(__dirname, 'hooks'), join(CLAUDE_DIR, 'hooks'), 'hooks → ~/.claude/hooks/');
124
+ // 5. Hooks (copy scripts only, skip hooks.json to avoid overwriting other plugins)
125
+ for (const hookFile of HOOK_FILES) {
126
+ copyFile(join(__dirname, 'hooks', hookFile), join(CLAUDE_DIR, 'hooks', hookFile), `hooks/${hookFile}`);
127
+ }
114
128
 
115
129
  // 6. Stable runtime for MCP server
116
130
  copyDir(join(__dirname, 'src'), join(RUNTIME_DIR, 'src'), 'runtime/src → ~/.claude/gsd/src/');
@@ -148,18 +162,20 @@ export function main() {
148
162
  args: [join(RUNTIME_DIR, 'src', 'server.js')],
149
163
  };
150
164
 
151
- // Register statusLine (top-level setting) and PostToolUse hook
165
+ // Register statusLine (top-level setting) and hooks
152
166
  if (!settings.hooks) settings.hooks = {};
153
167
  const statuslinePath = join(CLAUDE_DIR, 'hooks', 'gsd-statusline.cjs');
154
- const contextMonitorPath = join(CLAUDE_DIR, 'hooks', 'gsd-context-monitor.cjs');
155
168
  const statusLineRegistered = registerStatusLine(settings, statuslinePath);
156
- const postToolUseRegistered = registerPostToolUseHook(settings.hooks, contextMonitorPath);
169
+ let hooksRegistered = false;
170
+ for (const config of HOOK_REGISTRY) {
171
+ if (registerHookEntry(settings.hooks, config)) hooksRegistered = true;
172
+ }
157
173
 
158
174
  const tmpSettings = settingsPath + `.${process.pid}-${Date.now()}.tmp`;
159
175
  writeFileSync(tmpSettings, JSON.stringify(settings, null, 2) + '\n');
160
176
  renameSync(tmpSettings, settingsPath);
161
177
  log(' ✓ MCP server registered in settings.json');
162
- if (statusLineRegistered || postToolUseRegistered) {
178
+ if (statusLineRegistered || hooksRegistered) {
163
179
  log(' ✓ GSD-Lite hooks registered in settings.json');
164
180
  }
165
181
  } else {
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.13",
3
+ "version": "0.3.15",
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
@@ -793,6 +793,9 @@ export async function handleExecutorResult({ result, basePath = process.cwd() }
793
793
  };
794
794
  }
795
795
 
796
+ // Task stays in 'running' lifecycle intentionally — executor outcome 'failed' means
797
+ // "attempt failed, ready for retry or debugger", NOT lifecycle 'failed'. The task only
798
+ // transitions to lifecycle 'failed' via handleDebuggerResult when debugging is exhausted.
796
799
  const retry_count = (task.retry_count || 0) + 1;
797
800
  const error_fingerprint = typeof result.error_fingerprint === 'string' && result.error_fingerprint.length > 0
798
801
  ? result.error_fingerprint
@@ -967,7 +970,10 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
967
970
  }
968
971
  }
969
972
 
970
- // Snapshot accepted task IDs before propagation (for done counter adjustment)
973
+ // Snapshot accepted task IDs before propagation (for done counter adjustment).
974
+ // Note: rework_tasks patches above are NOT yet applied in-memory, so tasks demoted
975
+ // by the rework loop are still 'accepted' here. The guard below
976
+ // `!taskPatches.some(p => p.id === task.id)` prevents double-counting.
971
977
  const acceptedBeforePropagation = new Set(
972
978
  (phase.todo || []).filter(t => t.lifecycle === 'accepted').map(t => t.id),
973
979
  );
File without changes
File without changes
package/src/utils.js CHANGED
File without changes
package/uninstall.js CHANGED
@@ -38,7 +38,7 @@ export function main() {
38
38
  removeDir(join(CLAUDE_DIR, 'gsd-lite'), 'legacy gsd-lite runtime/');
39
39
 
40
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']) {
41
+ for (const name of ['context-monitor.js', 'gsd-statusline.cjs', 'gsd-context-monitor.cjs', 'gsd-session-init.cjs', 'gsd-auto-update.cjs']) {
42
42
  const hookFile = join(CLAUDE_DIR, 'hooks', name);
43
43
  if (existsSync(hookFile)) {
44
44
  rmSync(hookFile);
@@ -118,19 +118,23 @@ export function main() {
118
118
  delete settings.hooks.StatusLine;
119
119
  changed = true;
120
120
  }
121
- // Remove GSD PostToolUse entry from array (match both old and new patterns)
122
- if (Array.isArray(settings.hooks.PostToolUse)) {
123
- const len = settings.hooks.PostToolUse.length;
124
- settings.hooks.PostToolUse = settings.hooks.PostToolUse.filter(e =>
125
- !e.hooks?.some(h => h.command?.includes('gsd-context-monitor') ||
126
- h.command?.includes('context-monitor.js')));
127
- if (settings.hooks.PostToolUse.length < len) changed = true;
128
- if (settings.hooks.PostToolUse.length === 0) delete settings.hooks.PostToolUse;
129
- } else if (typeof settings.hooks.PostToolUse === 'string'
130
- && (settings.hooks.PostToolUse.includes('gsd-context-monitor') ||
131
- settings.hooks.PostToolUse.includes('context-monitor.js'))) {
132
- delete settings.hooks.PostToolUse;
133
- changed = true;
121
+ // Remove GSD entries from hook arrays
122
+ for (const [hookType, identifier] of [
123
+ ['PostToolUse', 'gsd-context-monitor'],
124
+ ['PostToolUse', 'context-monitor.js'],
125
+ ['SessionStart', 'gsd-session-init'],
126
+ ]) {
127
+ if (Array.isArray(settings.hooks[hookType])) {
128
+ const len = settings.hooks[hookType].length;
129
+ settings.hooks[hookType] = settings.hooks[hookType].filter(e =>
130
+ !e.hooks?.some(h => h.command?.includes(identifier)));
131
+ if (settings.hooks[hookType].length < len) changed = true;
132
+ if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
133
+ } else if (typeof settings.hooks[hookType] === 'string'
134
+ && settings.hooks[hookType].includes(identifier)) {
135
+ delete settings.hooks[hookType];
136
+ changed = true;
137
+ }
134
138
  }
135
139
  }
136
140
  if (changed) {
File without changes
File without changes
File without changes
File without changes
File without changes