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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +0 -0
- package/README.md +2 -2
- package/agents/debugger.md +0 -0
- package/agents/executor.md +0 -0
- package/agents/researcher.md +0 -0
- package/agents/reviewer.md +0 -0
- package/cli.js +15 -0
- package/commands/prd.md +1 -1
- package/commands/resume.md +0 -0
- package/commands/start.md +1 -1
- package/commands/status.md +0 -0
- package/commands/stop.md +0 -0
- package/hooks/context-monitor.js +0 -0
- package/hooks/gsd-auto-update.cjs +288 -0
- package/hooks/gsd-context-monitor.cjs +0 -0
- package/hooks/gsd-session-init.cjs +62 -45
- package/hooks/gsd-statusline.cjs +0 -0
- package/hooks/hooks.json +1 -1
- package/install.js +37 -21
- package/launcher.js +0 -0
- package/package.json +1 -1
- package/references/anti-rationalization-full.md +0 -0
- package/references/evidence-spec.md +0 -0
- package/references/execution-loop.md +0 -0
- package/references/git-worktrees.md +0 -0
- package/references/questioning.md +0 -0
- package/references/review-classification.md +0 -0
- package/references/state-diagram.md +0 -0
- package/references/testing-patterns.md +0 -0
- package/src/schema.js +0 -0
- package/src/server.js +0 -0
- package/src/tools/orchestrator.js +7 -1
- package/src/tools/state.js +0 -0
- package/src/tools/verify.js +0 -0
- package/src/utils.js +0 -0
- package/uninstall.js +18 -14
- package/workflows/debugging.md +0 -0
- package/workflows/deviation-rules.md +0 -0
- package/workflows/research.md +0 -0
- package/workflows/review-cycle.md +0 -0
- 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.
|
|
16
|
+
"version": "0.3.15",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
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/ #
|
|
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
|
|
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
|
package/agents/debugger.md
CHANGED
|
File without changes
|
package/agents/executor.md
CHANGED
|
File without changes
|
package/agents/researcher.md
CHANGED
|
File without changes
|
package/agents/reviewer.md
CHANGED
|
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
|
|
package/commands/resume.md
CHANGED
|
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
|
|
package/commands/status.md
CHANGED
|
File without changes
|
package/commands/stop.md
CHANGED
|
File without changes
|
package/hooks/context-monitor.js
CHANGED
|
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
|
-
//
|
|
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
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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(() => {});
|
package/hooks/gsd-statusline.cjs
CHANGED
|
File without changes
|
package/hooks/hooks.json
CHANGED
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
|
-
|
|
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
|
|
33
|
-
const
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
41
|
-
if (!hooks.
|
|
42
|
-
log(
|
|
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
|
|
57
|
+
hooks[hookType] = [entry];
|
|
46
58
|
return true;
|
|
47
59
|
}
|
|
48
|
-
if (Array.isArray(hooks
|
|
49
|
-
const idx = hooks.
|
|
50
|
-
e.hooks?.some(h => h.command?.includes(
|
|
51
|
-
if (idx >= 0) hooks
|
|
52
|
-
else hooks.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 ||
|
|
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
|
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
|
);
|
package/src/tools/state.js
CHANGED
|
File without changes
|
package/src/tools/verify.js
CHANGED
|
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
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (settings.hooks
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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) {
|
package/workflows/debugging.md
CHANGED
|
File without changes
|
|
File without changes
|
package/workflows/research.md
CHANGED
|
File without changes
|
|
File without changes
|
package/workflows/tdd-cycle.md
CHANGED
|
File without changes
|