gsd-lite 0.5.14 → 0.6.0
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 +0 -0
- package/agents/debugger.md +11 -4
- package/agents/executor.md +0 -0
- package/agents/researcher.md +0 -0
- package/agents/reviewer.md +12 -1
- package/commands/doctor.md +0 -1
- package/commands/prd.md +18 -1
- package/commands/resume.md +27 -21
- package/commands/start.md +8 -0
- 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 +72 -15
- package/hooks/gsd-context-monitor.cjs +0 -0
- package/hooks/gsd-session-init.cjs +0 -0
- package/hooks/gsd-session-stop.cjs +0 -0
- package/hooks/gsd-statusline.cjs +0 -0
- package/hooks/hooks.json +0 -0
- package/hooks/lib/gsd-finder.cjs +19 -3
- package/hooks/lib/semver-sort.cjs +20 -0
- package/install.js +84 -7
- 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 +1 -1
- package/references/git-worktrees.md +0 -0
- package/references/questioning.md +0 -0
- package/references/review-classification.md +25 -6
- package/references/state-diagram.md +4 -3
- package/references/testing-patterns.md +0 -0
- package/src/schema.js +53 -30
- package/src/server.js +0 -0
- package/src/tools/orchestrator/debugger.js +0 -0
- package/src/tools/orchestrator/executor.js +10 -0
- package/src/tools/orchestrator/helpers.js +16 -9
- package/src/tools/orchestrator/index.js +0 -0
- package/src/tools/orchestrator/researcher.js +0 -0
- package/src/tools/orchestrator/resume.js +252 -188
- package/src/tools/orchestrator/reviewer.js +0 -0
- package/src/tools/state/constants.js +2 -0
- package/src/tools/state/crud.js +50 -44
- package/src/tools/state/index.js +0 -0
- package/src/tools/state/logic.js +0 -0
- package/src/tools/verify.js +0 -0
- package/src/utils.js +0 -0
- package/uninstall.js +12 -1
- package/workflows/debugging.md +14 -11
- package/workflows/deviation-rules.md +4 -4
- package/workflows/execution-flow.md +0 -0
- package/workflows/research.md +0 -0
- package/workflows/review-cycle.md +19 -2
- 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.
|
|
16
|
+
"version": "0.6.0",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
package/.mcp.json
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
package/agents/debugger.md
CHANGED
|
@@ -55,8 +55,11 @@ Phase 3 假设测试:
|
|
|
55
55
|
3. 验证: 有效 → Phase 4 / 无效 → 新假设
|
|
56
56
|
|
|
57
57
|
Phase 4 修复方向建议:
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
调试器不直接写代码 — 返回 fix_direction + 测试用例描述,由 executor 实施。
|
|
59
|
+
1. 提出修复方案 (针对根因,不是症状) → 写入 `fix_direction`
|
|
60
|
+
2. 描述回归测试用例 (测什么、预期 vs 实际) → 供 executor 实现
|
|
61
|
+
- 不要自己写测试代码,你没有 Write 工具
|
|
62
|
+
- 用自然语言描述: 输入、操作步骤、预期结果、实际错误行为
|
|
60
63
|
3. 评估修复影响范围 (哪些下游可能受影响)
|
|
61
64
|
→ 3 次修复方向均被 executor 验证无效 → 停止。标记 architecture_concern: true。报告给编排器。
|
|
62
65
|
</four_phases>
|
|
@@ -66,6 +69,10 @@ Phase 4 修复方向建议:
|
|
|
66
69
|
{
|
|
67
70
|
"task_id": "2.3",
|
|
68
71
|
"outcome": "root_cause_found | fix_suggested | failed",
|
|
72
|
+
// outcome 判定:
|
|
73
|
+
// root_cause_found — 根因已确认,有明确的修复方向
|
|
74
|
+
// fix_suggested — 部分理解,建议一个尝试方向 (根因尚未完全确认)
|
|
75
|
+
// failed — 穷尽假设仍无法定位根因,或 fix_attempts >= 3
|
|
69
76
|
"root_cause": "Description of the identified root cause",
|
|
70
77
|
"evidence": [
|
|
71
78
|
{ "id": "ev:repro:error-xyz", "scope": "task:2.3", "command": "npm test", "exit_code": 1, "stdout": "...", "stderr": "...", "timestamp": "ISO8601" }
|
|
@@ -73,8 +80,8 @@ Phase 4 修复方向建议:
|
|
|
73
80
|
"hypothesis_tested": [
|
|
74
81
|
{ "hypothesis": "X causes Y", "result": "confirmed | rejected", "evidence": "non-empty string (required)" }
|
|
75
82
|
],
|
|
76
|
-
"fix_direction": "Suggested fix approach for executor",
|
|
77
|
-
"fix_attempts": 0,
|
|
83
|
+
"fix_direction": "Suggested fix approach for executor (include suggested test case description)",
|
|
84
|
+
"fix_attempts": 0, // 由编排器 dispatch context 中的 debug_context 传入,记录已尝试的修复方向次数
|
|
78
85
|
"blockers": [],
|
|
79
86
|
"architecture_concern": false
|
|
80
87
|
}
|
package/agents/executor.md
CHANGED
|
File without changes
|
package/agents/researcher.md
CHANGED
|
File without changes
|
package/agents/reviewer.md
CHANGED
|
@@ -51,6 +51,14 @@ L1 普通编码任务 → executor 自审 + 阶段结束时批量 review
|
|
|
51
51
|
L2 关键任务 → 单任务独立 review
|
|
52
52
|
(涉及认证/支付/数据安全/核心架构的任务)
|
|
53
53
|
|
|
54
|
+
L3 最高风险任务 → 单任务独立 review + 人工确认
|
|
55
|
+
(auth/payment/security architecture 等最高风险任务)
|
|
56
|
+
- 与 L2 相同的双阶段审查流程,外加:
|
|
57
|
+
- 结果中必须包含 `requires_human_confirmation: true`
|
|
58
|
+
- review summary 必须明确列出安全影响 (security implications)
|
|
59
|
+
- 质量审查阶段必须检查 OWASP Top 10 相关问题
|
|
60
|
+
- 审查通过后 task 进入 `awaiting_user` 而非 `accepted`,需用户显式确认
|
|
61
|
+
|
|
54
62
|
判定规则按影响面,不按关键词猜测:
|
|
55
63
|
- 改 auth/payment/permission/public API/DB migration/core architecture → L2
|
|
56
64
|
- 纯 docs/comment/style/config 且无运行时语义变化 → L0
|
|
@@ -107,7 +115,9 @@ Minor = 建议修复 (命名/风格)
|
|
|
107
115
|
{
|
|
108
116
|
"scope": "task | phase",
|
|
109
117
|
"scope_id": "2.3 (task scope: string ID) | 2 (phase scope: number ID)",
|
|
110
|
-
"review_level": "L2 | L1-batch | L1",
|
|
118
|
+
"review_level": "L2 | L3 | L1-batch | L1",
|
|
119
|
+
"requires_human_confirmation": false, // L3 时必须为 true
|
|
120
|
+
"security_implications": [], // L3 时必须列出安全影响
|
|
111
121
|
"spec_passed": true,
|
|
112
122
|
"quality_passed": false,
|
|
113
123
|
"critical_issues": [
|
|
@@ -141,4 +151,5 @@ checkpoint commit ≠ accepted
|
|
|
141
151
|
L0: checkpoint commit = accepted
|
|
142
152
|
L1: checkpoint commit → phase batch review 通过 → accepted
|
|
143
153
|
L2: checkpoint commit → immediate independent review 通过 → accepted
|
|
154
|
+
L3: checkpoint commit → immediate independent review 通过 → awaiting_user → 用户确认 → accepted
|
|
144
155
|
</checkpoint_topology>
|
package/commands/doctor.md
CHANGED
package/commands/prd.md
CHANGED
|
@@ -17,13 +17,30 @@ argument-hint: File path to requirements doc, or inline description text
|
|
|
17
17
|
|
|
18
18
|
<process>
|
|
19
19
|
|
|
20
|
+
## STEP 0 — 已有项目检测
|
|
21
|
+
|
|
22
|
+
调用 `health` 工具(MCP tool 名称: health)。如果返回 state_exists=true 且项目未完成/未失败:
|
|
23
|
+
- 告知用户: "检测到进行中的 GSD 项目。"
|
|
24
|
+
- 显示当前项目状态 (项目名、当前阶段、workflow_mode)
|
|
25
|
+
- 提供选项:
|
|
26
|
+
- (a) 恢复现有项目 → 转到 `/gsd:resume`
|
|
27
|
+
- (b) 覆盖并重新开始 → 继续 STEP 1(现有 state.json 将被覆盖)
|
|
28
|
+
- (c) 取消
|
|
29
|
+
- 等待用户选择后再继续
|
|
30
|
+
|
|
31
|
+
如果无 state 或项目已完成/已失败 → 直接进入 STEP 1。
|
|
32
|
+
|
|
20
33
|
## STEP 1: 解析输入
|
|
21
34
|
|
|
22
35
|
判断 `$ARGUMENTS` 的类型:
|
|
23
36
|
|
|
24
37
|
**如果是文件路径** (包含 `/` 或 `.` 且文件存在):
|
|
25
38
|
- 使用 Read 工具读取文件内容
|
|
26
|
-
-
|
|
39
|
+
- 如果文件不存在:
|
|
40
|
+
- 告知用户文件不存在
|
|
41
|
+
- 提示常见原因: 路径拼写错误、当前工作目录不正确
|
|
42
|
+
- 建议: "请确认文件路径,或使用绝对路径重试。需要我帮你查找文件吗?"
|
|
43
|
+
- 停止
|
|
27
44
|
|
|
28
45
|
**如果是文本描述**:
|
|
29
46
|
- 直接作为需求描述使用
|
package/commands/resume.md
CHANGED
|
@@ -9,19 +9,20 @@ description: Resume project execution from saved state with workspace validation
|
|
|
9
9
|
|
|
10
10
|
<process>
|
|
11
11
|
|
|
12
|
-
## STEP 1:
|
|
12
|
+
## STEP 1: 调用 orchestrator-resume 获取状态摘要
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
14
|
+
调用 MCP tool `orchestrator-resume`,使用响应中的 `summary` 字段展示状态给用户:
|
|
15
|
+
- 如果响应为 error 且 message 包含 "No .gsd directory" → 告知用户 "未找到 GSD 项目状态,请先运行 /gsd:start 或 /gsd:prd",停止
|
|
16
|
+
- 如果响应为 error → 告知用户错误信息并停止
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
`summary` 字段包含:
|
|
19
19
|
- `workflow_mode` — 当前工作流状态
|
|
20
|
-
- `current_phase`
|
|
21
|
-
- `
|
|
22
|
-
- `
|
|
23
|
-
- `
|
|
24
|
-
|
|
20
|
+
- `current_phase` — 当前阶段 (格式: "N/M")
|
|
21
|
+
- `current_task` — 当前任务 (id + name)
|
|
22
|
+
- `phase_progress` — 阶段进度 (格式: "done/total")
|
|
23
|
+
- `recent_decisions` — 最近 2-3 个决策 (如有)
|
|
24
|
+
|
|
25
|
+
注意: 不需要单独读取 state.json,`orchestrator-resume` 的响应已包含所有需要展示的信息。
|
|
25
26
|
|
|
26
27
|
## STEP 2: 前置校验
|
|
27
28
|
|
|
@@ -55,10 +56,15 @@ description: Resume project execution from saved state with workspace validation
|
|
|
55
56
|
- 或 research.decision_index 中有条目的 expires_at 已过期
|
|
56
57
|
- → 覆写 `workflow_mode = research_refresh_needed`
|
|
57
58
|
|
|
58
|
-
5.
|
|
59
|
+
5. **Dirty-phase 回滚检测:**
|
|
60
|
+
- 检查已完成 phase 中是否有 `needs_revalidation` 状态的 task
|
|
61
|
+
- 如有 → 回滚 `current_phase` 到最早的 dirty phase
|
|
62
|
+
- → 覆写 `workflow_mode = executing_task`
|
|
63
|
+
|
|
64
|
+
6. **全部通过:**
|
|
59
65
|
- 保持原 `workflow_mode` 不变
|
|
60
66
|
|
|
61
|
-
校验顺序: 1→2→3→4,首个命中的覆写生效 (不累积)
|
|
67
|
+
校验顺序: 1→2→3→4→5,首个命中的覆写生效 (不累积)
|
|
62
68
|
</HARD-GATE>
|
|
63
69
|
|
|
64
70
|
## STEP 3: 按 workflow_mode 恢复
|
|
@@ -106,8 +112,8 @@ description: Resume project execution from saved state with workspace validation
|
|
|
106
112
|
### `awaiting_clear` — 继续自动执行
|
|
107
113
|
|
|
108
114
|
- 上下文已通过 /clear 清理
|
|
109
|
-
-
|
|
110
|
-
-
|
|
115
|
+
- 再次验证上下文健康度 ≥ 40%,不足则要求再次 /clear
|
|
116
|
+
- 验证通过后从 `current_phase` + `current_task` 恢复调度
|
|
111
117
|
|
|
112
118
|
---
|
|
113
119
|
|
|
@@ -208,17 +214,17 @@ description: Resume project execution from saved state with workspace validation
|
|
|
208
214
|
|
|
209
215
|
## STEP 4: 显示当前进度 + 下一动作
|
|
210
216
|
|
|
211
|
-
|
|
217
|
+
每次恢复后使用 `orchestrator-resume` 响应中的 `summary` 字段展示简要进度面板:
|
|
212
218
|
|
|
213
219
|
```
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
下一步: {根据
|
|
220
|
+
模式: {summary.workflow_mode}
|
|
221
|
+
进度: Phase {summary.current_phase} | Task {summary.phase_progress}
|
|
222
|
+
当前: {summary.current_task.id} — {summary.current_task.name}
|
|
223
|
+
决策: {summary.recent_decisions (如有)}
|
|
224
|
+
下一步: {根据 action 推导的下一动作}
|
|
219
225
|
```
|
|
220
226
|
|
|
221
|
-
|
|
227
|
+
注意: 所有展示数据直接取自 `summary` 字段,不需要额外读取 state.json。
|
|
222
228
|
|
|
223
229
|
## STEP 5: 自动执行循环
|
|
224
230
|
|
package/commands/start.md
CHANGED
|
@@ -58,3 +58,11 @@ argument-hint: Optional feature or project description
|
|
|
58
58
|
使用 Read 工具读取 `workflows/execution-flow.md`,严格按照其中 STEP 5-12 执行。
|
|
59
59
|
|
|
60
60
|
</process>
|
|
61
|
+
|
|
62
|
+
<EXTREMELY-IMPORTANT>
|
|
63
|
+
## 编排器纪律
|
|
64
|
+
- 只有编排器写 state.json,子代理不直接写
|
|
65
|
+
- 所有摘要/提示在展示时从 canonical fields 推导,不持久化 derived fields
|
|
66
|
+
- 子代理返回结构化 JSON,不解析自然语言
|
|
67
|
+
- 上下文 < 35% → 保存状态 + workflow_mode = awaiting_clear + 停止执行
|
|
68
|
+
</EXTREMELY-IMPORTANT>
|
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
|
|
@@ -7,6 +7,7 @@ const fs = require('node:fs');
|
|
|
7
7
|
const path = require('node:path');
|
|
8
8
|
const os = require('node:os');
|
|
9
9
|
const { execSync, spawnSync } = require('node:child_process');
|
|
10
|
+
const { semverSortComparator } = require('./lib/semver-sort.cjs');
|
|
10
11
|
|
|
11
12
|
// ── Configuration ──────────────────────────────────────────
|
|
12
13
|
const GITHUB_REPO = 'sdsrss/gsd-lite';
|
|
@@ -195,12 +196,13 @@ async function withFileLock(fn) {
|
|
|
195
196
|
}
|
|
196
197
|
}
|
|
197
198
|
|
|
199
|
+
if (!acquired) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
198
202
|
try {
|
|
199
203
|
return await fn();
|
|
200
204
|
} finally {
|
|
201
|
-
|
|
202
|
-
try { fs.rmSync(STATE_LOCK_FILE, { force: true }); } catch {}
|
|
203
|
-
}
|
|
205
|
+
try { fs.rmSync(STATE_LOCK_FILE, { force: true }); } catch {}
|
|
204
206
|
}
|
|
205
207
|
}
|
|
206
208
|
|
|
@@ -209,7 +211,7 @@ async function withFileLock(fn) {
|
|
|
209
211
|
// 1. PLUGIN_AUTO_UPDATE env set → recursive guard (auto-update already in progress)
|
|
210
212
|
// 2. Running from a git clone → dev mode (developer working on source)
|
|
211
213
|
function shouldSkipUpdateCheck() {
|
|
212
|
-
if (process.env.PLUGIN_AUTO_UPDATE) return true;
|
|
214
|
+
if (process.env.PLUGIN_AUTO_UPDATE === '1') return true;
|
|
213
215
|
return isDevMode();
|
|
214
216
|
}
|
|
215
217
|
|
|
@@ -297,15 +299,8 @@ async function fetchLatestRelease(token) {
|
|
|
297
299
|
}
|
|
298
300
|
|
|
299
301
|
// ── Version Comparison (semver) ────────────────────────────
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
const pb = b.split('.').map(Number);
|
|
303
|
-
for (let i = 0; i < 3; i++) {
|
|
304
|
-
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
305
|
-
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
306
|
-
}
|
|
307
|
-
return 0;
|
|
308
|
-
}
|
|
302
|
+
// Reuse shared comparator; callers only check sign (> 0, < 0, === 0)
|
|
303
|
+
const compareVersions = semverSortComparator;
|
|
309
304
|
|
|
310
305
|
function getCurrentVersion(mode = getInstallMode()) {
|
|
311
306
|
const candidates = mode === 'manual'
|
|
@@ -337,7 +332,7 @@ function validateExtractedPackage(extractDir) {
|
|
|
337
332
|
// ── Download & Install ─────────────────────────────────────
|
|
338
333
|
async function downloadAndInstall(tarballUrl, verbose = false, token = null) {
|
|
339
334
|
const tmpDir = path.join(os.tmpdir(), `gsd-update-${Date.now()}`);
|
|
340
|
-
const backupPath = path.join(
|
|
335
|
+
const backupPath = path.join(runtimeDir, 'package.json.bak');
|
|
341
336
|
let backedUp = false;
|
|
342
337
|
try {
|
|
343
338
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
@@ -450,6 +445,42 @@ function writeNotification(notification) {
|
|
|
450
445
|
}
|
|
451
446
|
}
|
|
452
447
|
|
|
448
|
+
// ── Cache Cleanup ─────────────────────────────────────────
|
|
449
|
+
// Remove old plugin cache versions, keeping the N most recent.
|
|
450
|
+
function pruneOldCacheVersions(cacheBase, keepCount = 3, verbose = false) {
|
|
451
|
+
try {
|
|
452
|
+
if (!fs.existsSync(cacheBase)) return;
|
|
453
|
+
const entries = fs.readdirSync(cacheBase, { withFileTypes: true })
|
|
454
|
+
.filter(e => e.isDirectory())
|
|
455
|
+
.map(e => e.name);
|
|
456
|
+
if (entries.length <= keepCount) return;
|
|
457
|
+
|
|
458
|
+
// Sort by semver using shared comparator
|
|
459
|
+
const sorted = entries.slice().sort(semverSortComparator);
|
|
460
|
+
|
|
461
|
+
// Detect versions with active processes to avoid disrupting running sessions
|
|
462
|
+
let activeVersions;
|
|
463
|
+
try {
|
|
464
|
+
const psOutput = spawnSync('ps', ['aux'], { stdio: 'pipe', timeout: 5000 });
|
|
465
|
+
const lines = (psOutput.stdout || '').toString();
|
|
466
|
+
activeVersions = new Set(
|
|
467
|
+
entries.filter(ver => lines.includes(`/cache/gsd/gsd/${ver}/`))
|
|
468
|
+
);
|
|
469
|
+
} catch { activeVersions = new Set(); }
|
|
470
|
+
|
|
471
|
+
const toRemove = sorted.slice(0, sorted.length - keepCount);
|
|
472
|
+
for (const ver of toRemove) {
|
|
473
|
+
if (activeVersions.has(ver)) {
|
|
474
|
+
if (verbose) console.log(` Skipped ${ver} (active process detected)`);
|
|
475
|
+
continue;
|
|
476
|
+
}
|
|
477
|
+
const verPath = path.join(cacheBase, ver);
|
|
478
|
+
fs.rmSync(verPath, { recursive: true, force: true });
|
|
479
|
+
if (verbose) console.log(` Pruned old cache: ${ver}`);
|
|
480
|
+
}
|
|
481
|
+
} catch { /* best effort */ }
|
|
482
|
+
}
|
|
483
|
+
|
|
453
484
|
// ── Plugin Cache Sync ─────────────────────────────────────
|
|
454
485
|
// When installed as a plugin, the MCP server runs from plugins/cache/gsd/gsd/<version>/
|
|
455
486
|
// The auto-update installs to ~/.claude/gsd/ (runtime dir) via install.js,
|
|
@@ -492,11 +523,17 @@ function syncPluginCache(extractedDir, verbose = false) {
|
|
|
492
523
|
|
|
493
524
|
// Install dependencies in cache dir
|
|
494
525
|
if (!fs.existsSync(path.join(newCachePath, 'node_modules', '@modelcontextprotocol'))) {
|
|
495
|
-
spawnSync('npm', ['install', '--omit=dev', '--ignore-scripts'], {
|
|
526
|
+
const npmResult = spawnSync('npm', ['install', '--omit=dev', '--ignore-scripts'], {
|
|
496
527
|
cwd: newCachePath,
|
|
497
528
|
stdio: 'pipe',
|
|
498
529
|
timeout: 60000,
|
|
499
530
|
});
|
|
531
|
+
if (npmResult.status !== 0) {
|
|
532
|
+
// npm install failed — don't update registry to point to broken cache
|
|
533
|
+
if (verbose) console.error(' npm install failed in cache dir, aborting cache sync');
|
|
534
|
+
fs.rmSync(newCachePath, { recursive: true, force: true });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
500
537
|
}
|
|
501
538
|
|
|
502
539
|
// Update installed_plugins.json to point to new cache path
|
|
@@ -507,6 +544,26 @@ function syncPluginCache(extractedDir, verbose = false) {
|
|
|
507
544
|
fs.writeFileSync(tmpPlugins, JSON.stringify(plugins, null, 2) + '\n');
|
|
508
545
|
fs.renameSync(tmpPlugins, pluginsFile);
|
|
509
546
|
|
|
547
|
+
// Update settings.json statusLine if it points to the old cache path
|
|
548
|
+
try {
|
|
549
|
+
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
550
|
+
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
551
|
+
if (settings.statusLine?.command?.includes('/plugins/cache/gsd/gsd/')) {
|
|
552
|
+
const oldCmd = settings.statusLine.command;
|
|
553
|
+
const updated = oldCmd.replace(/\/plugins\/cache\/gsd\/gsd\/[^/]+\//g, `/plugins/cache/gsd/gsd/${newVersion}/`);
|
|
554
|
+
if (updated !== oldCmd) {
|
|
555
|
+
settings.statusLine.command = updated;
|
|
556
|
+
const tmpSettings = settingsPath + `.${process.pid}.tmp`;
|
|
557
|
+
fs.writeFileSync(tmpSettings, JSON.stringify(settings, null, 2) + '\n');
|
|
558
|
+
fs.renameSync(tmpSettings, settingsPath);
|
|
559
|
+
if (verbose) console.log(' StatusLine path updated to new version');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
} catch {}
|
|
563
|
+
|
|
564
|
+
// Prune old cache versions — keep only the 3 most recent
|
|
565
|
+
pruneOldCacheVersions(cacheBase, 3, verbose);
|
|
566
|
+
|
|
510
567
|
if (verbose) console.log(` Plugin cache synced to v${newVersion}`);
|
|
511
568
|
} catch (err) {
|
|
512
569
|
// Best effort — don't fail the update if cache sync fails
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/hooks/gsd-statusline.cjs
CHANGED
|
File without changes
|
package/hooks/hooks.json
CHANGED
|
File without changes
|
package/hooks/lib/gsd-finder.cjs
CHANGED
|
@@ -9,26 +9,42 @@
|
|
|
9
9
|
const fs = require('node:fs');
|
|
10
10
|
const path = require('node:path');
|
|
11
11
|
|
|
12
|
+
const _findCache = new Map();
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Walk from startDir up to filesystem root looking for a .gsd directory
|
|
14
16
|
* that contains state.json. Returns the absolute path to .gsd or null.
|
|
17
|
+
* Results are cached per startDir (positive hits only — null is not cached
|
|
18
|
+
* so that a later-created .gsd directory can be discovered).
|
|
15
19
|
*/
|
|
16
20
|
function findGsdDir(startDir) {
|
|
21
|
+
if (_findCache.has(startDir)) return _findCache.get(startDir);
|
|
22
|
+
|
|
17
23
|
let dir = startDir;
|
|
18
24
|
while (true) {
|
|
19
25
|
const candidate = path.join(dir, '.gsd');
|
|
20
26
|
try {
|
|
21
27
|
if (fs.statSync(candidate).isDirectory()) {
|
|
22
28
|
// Only return if state.json exists (not just an empty .gsd dir)
|
|
23
|
-
if (fs.existsSync(path.join(candidate, 'state.json')))
|
|
29
|
+
if (fs.existsSync(path.join(candidate, 'state.json'))) {
|
|
30
|
+
_findCache.set(startDir, candidate);
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|
|
24
33
|
}
|
|
25
34
|
} catch { /* skip */ }
|
|
26
35
|
const parent = path.dirname(dir);
|
|
27
|
-
if (parent === dir) return null;
|
|
36
|
+
if (parent === dir) return null; // Don't cache negative results
|
|
28
37
|
dir = parent;
|
|
29
38
|
}
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Clear the findGsdDir result cache. Useful for testing.
|
|
43
|
+
*/
|
|
44
|
+
function clearFindGsdDirCache() {
|
|
45
|
+
_findCache.clear();
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
/**
|
|
33
49
|
* Read and parse .gsd/state.json. Returns parsed object or null on any failure.
|
|
34
50
|
*/
|
|
@@ -81,4 +97,4 @@ function getProgress(state) {
|
|
|
81
97
|
};
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
module.exports = { findGsdDir, readState, getProgress };
|
|
100
|
+
module.exports = { findGsdDir, clearFindGsdDirCache, readState, getProgress };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Shared semver sort comparator for use by install.js and gsd-auto-update.cjs
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compare two semver version strings (e.g. "1.2.3") for sorting.
|
|
6
|
+
* Returns negative if a < b, positive if a > b, 0 if equal.
|
|
7
|
+
* @param {string} a
|
|
8
|
+
* @param {string} b
|
|
9
|
+
* @returns {number}
|
|
10
|
+
*/
|
|
11
|
+
function semverSortComparator(a, b) {
|
|
12
|
+
const pa = a.split('.').map(Number);
|
|
13
|
+
const pb = b.split('.').map(Number);
|
|
14
|
+
for (let i = 0; i < 3; i++) {
|
|
15
|
+
if ((pa[i] || 0) !== (pb[i] || 0)) return (pa[i] || 0) - (pb[i] || 0);
|
|
16
|
+
}
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
module.exports = { semverSortComparator };
|
package/install.js
CHANGED
|
@@ -1,24 +1,28 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Plugin installer for GSD-Lite
|
|
3
3
|
|
|
4
|
-
import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs';
|
|
4
|
+
import { existsSync, mkdirSync, cpSync, readFileSync, writeFileSync, renameSync, rmSync, readdirSync } from 'node:fs';
|
|
5
5
|
import { join, dirname } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
8
8
|
import { execSync } from 'node:child_process';
|
|
9
|
+
import { createRequire } from 'node:module';
|
|
9
10
|
|
|
10
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const _require = createRequire(import.meta.url);
|
|
13
|
+
const { semverSortComparator } = _require('./hooks/lib/semver-sort.cjs');
|
|
11
14
|
const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
|
12
15
|
const RUNTIME_DIR = join(CLAUDE_DIR, 'gsd');
|
|
13
16
|
const DRY_RUN = process.argv.includes('--dry-run');
|
|
14
17
|
|
|
15
18
|
// 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'];
|
|
19
|
+
const HOOK_FILES = ['gsd-session-init.cjs', 'gsd-auto-update.cjs', 'gsd-context-monitor.cjs', 'gsd-statusline.cjs', 'gsd-session-stop.cjs'];
|
|
17
20
|
|
|
18
21
|
// Hook registration config: hookType → { file identifier, matcher, timeout? }
|
|
19
22
|
const HOOK_REGISTRY = [
|
|
20
23
|
{ hookType: 'SessionStart', identifier: 'gsd-session-init', matcher: 'startup', timeout: 5 },
|
|
21
24
|
{ hookType: 'PostToolUse', identifier: 'gsd-context-monitor', matcher: '*' },
|
|
25
|
+
{ hookType: 'Stop', identifier: 'gsd-session-stop', matcher: '*', timeout: 3 },
|
|
22
26
|
];
|
|
23
27
|
|
|
24
28
|
function log(msg) { console.log(msg); }
|
|
@@ -115,8 +119,23 @@ export function main() {
|
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
// Reset managed runtime directory to avoid stale files on reinstall
|
|
122
|
+
// Preserve runtime/ subdirectory (update-state.json, update-notification.json)
|
|
118
123
|
if (!DRY_RUN && existsSync(RUNTIME_DIR)) {
|
|
124
|
+
const runtimeSubdir = join(RUNTIME_DIR, 'runtime');
|
|
125
|
+
const preserveRuntime = existsSync(runtimeSubdir);
|
|
126
|
+
let runtimeBackup;
|
|
127
|
+
if (preserveRuntime) {
|
|
128
|
+
runtimeBackup = join(RUNTIME_DIR, '..', '.gsd-runtime-backup');
|
|
129
|
+
try { cpSync(runtimeSubdir, runtimeBackup, { recursive: true }); } catch { runtimeBackup = null; }
|
|
130
|
+
}
|
|
119
131
|
rmSync(RUNTIME_DIR, { recursive: true, force: true });
|
|
132
|
+
if (runtimeBackup) {
|
|
133
|
+
try {
|
|
134
|
+
mkdirSync(join(RUNTIME_DIR, 'runtime'), { recursive: true });
|
|
135
|
+
cpSync(runtimeBackup, join(RUNTIME_DIR, 'runtime'), { recursive: true });
|
|
136
|
+
rmSync(runtimeBackup, { recursive: true, force: true });
|
|
137
|
+
} catch { /* best effort */ }
|
|
138
|
+
}
|
|
120
139
|
}
|
|
121
140
|
|
|
122
141
|
// 1. Commands
|
|
@@ -135,6 +154,11 @@ export function main() {
|
|
|
135
154
|
for (const hookFile of HOOK_FILES) {
|
|
136
155
|
copyFile(join(__dirname, 'hooks', hookFile), join(CLAUDE_DIR, 'hooks', hookFile), `hooks/${hookFile}`);
|
|
137
156
|
}
|
|
157
|
+
// 5b. Hook library dependencies (e.g. gsd-finder.cjs used by statusline + session-init)
|
|
158
|
+
const hookLibDir = join(__dirname, 'hooks', 'lib');
|
|
159
|
+
if (existsSync(hookLibDir)) {
|
|
160
|
+
copyDir(hookLibDir, join(CLAUDE_DIR, 'hooks', 'lib'), 'hooks/lib → ~/.claude/hooks/lib/');
|
|
161
|
+
}
|
|
138
162
|
|
|
139
163
|
// 6. Stable runtime for MCP server
|
|
140
164
|
copyDir(join(__dirname, 'src'), join(RUNTIME_DIR, 'src'), 'runtime/src → ~/.claude/gsd/src/');
|
|
@@ -186,12 +210,36 @@ export function main() {
|
|
|
186
210
|
}
|
|
187
211
|
|
|
188
212
|
// Register statusLine (top-level setting) and hooks
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
213
|
+
// When installed as a plugin, hooks are managed by hooks.json via the plugin system.
|
|
214
|
+
// Only register in settings.json for manual installs to avoid double execution.
|
|
215
|
+
let statusLineRegistered = false;
|
|
192
216
|
let hooksRegistered = false;
|
|
193
|
-
|
|
194
|
-
if (
|
|
217
|
+
if (!isPluginInstall) {
|
|
218
|
+
if (!settings.hooks) settings.hooks = {};
|
|
219
|
+
const statuslinePath = join(CLAUDE_DIR, 'hooks', 'gsd-statusline.cjs');
|
|
220
|
+
statusLineRegistered = registerStatusLine(settings, statuslinePath);
|
|
221
|
+
for (const config of HOOK_REGISTRY) {
|
|
222
|
+
if (registerHookEntry(settings.hooks, config)) hooksRegistered = true;
|
|
223
|
+
}
|
|
224
|
+
} else {
|
|
225
|
+
// Clean up stale manual hook entries left from previous install.js runs
|
|
226
|
+
if (settings.hooks) {
|
|
227
|
+
let cleaned = false;
|
|
228
|
+
for (const [hookType, identifier] of [
|
|
229
|
+
['PostToolUse', 'gsd-context-monitor'],
|
|
230
|
+
['SessionStart', 'gsd-session-init'],
|
|
231
|
+
['Stop', 'gsd-session-stop'],
|
|
232
|
+
]) {
|
|
233
|
+
if (Array.isArray(settings.hooks[hookType])) {
|
|
234
|
+
const before = settings.hooks[hookType].length;
|
|
235
|
+
settings.hooks[hookType] = settings.hooks[hookType].filter(e =>
|
|
236
|
+
!e.hooks?.some(h => h.command?.includes(identifier)));
|
|
237
|
+
if (settings.hooks[hookType].length < before) cleaned = true;
|
|
238
|
+
if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
if (cleaned) log(' ✓ Removed stale manual hook entries (plugin hooks.json handles registration)');
|
|
242
|
+
}
|
|
195
243
|
}
|
|
196
244
|
|
|
197
245
|
const tmpSettings = settingsPath + `.${process.pid}-${Date.now()}.tmp`;
|
|
@@ -204,6 +252,35 @@ export function main() {
|
|
|
204
252
|
log(' [dry-run] Would register MCP server in settings.json');
|
|
205
253
|
}
|
|
206
254
|
|
|
255
|
+
// 9. Prune old plugin cache versions (keep latest 3)
|
|
256
|
+
if (!DRY_RUN && isPluginInstall) {
|
|
257
|
+
const cacheBase = join(CLAUDE_DIR, 'plugins', 'cache', 'gsd', 'gsd');
|
|
258
|
+
if (existsSync(cacheBase)) {
|
|
259
|
+
try {
|
|
260
|
+
const entries = readdirSync(cacheBase, { withFileTypes: true })
|
|
261
|
+
.filter(e => e.isDirectory()).map(e => e.name);
|
|
262
|
+
if (entries.length > 3) {
|
|
263
|
+
const sorted = entries.slice().sort(semverSortComparator);
|
|
264
|
+
// Detect versions with active processes to avoid disrupting running sessions
|
|
265
|
+
let activeVersions;
|
|
266
|
+
try {
|
|
267
|
+
const psOut = execSync('ps aux', { stdio: 'pipe', timeout: 5000 }).toString();
|
|
268
|
+
activeVersions = new Set(entries.filter(v => psOut.includes(`/cache/gsd/gsd/${v}/`)));
|
|
269
|
+
} catch { activeVersions = new Set(); }
|
|
270
|
+
|
|
271
|
+
const toRemove = sorted.slice(0, sorted.length - 3);
|
|
272
|
+
let pruned = 0;
|
|
273
|
+
for (const ver of toRemove) {
|
|
274
|
+
if (activeVersions.has(ver)) continue; // skip versions with running processes
|
|
275
|
+
rmSync(join(cacheBase, ver), { recursive: true, force: true });
|
|
276
|
+
pruned++;
|
|
277
|
+
}
|
|
278
|
+
if (pruned > 0) log(` ✓ Pruned ${pruned} old cache version(s), kept latest 3`);
|
|
279
|
+
}
|
|
280
|
+
} catch { /* best effort */ }
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
207
284
|
log('\n✓ GSD-Lite installed successfully!');
|
|
208
285
|
log(' Use /gsd:start to begin a new project');
|
|
209
286
|
log(' Use /gsd:resume to continue an existing project');
|
package/launcher.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -81,7 +81,7 @@ executor 上下文传递协议 (orchestrator → executor):
|
|
|
81
81
|
**审查级别运行时重分类:**
|
|
82
82
|
- executor 报告 `contract_changed: true` + 涉及 auth/payment/public API → 自动升级为 L2
|
|
83
83
|
- executor 标注 `[LEVEL-UP]` → 编排器采纳
|
|
84
|
-
- 不主动降级 (安全优先)
|
|
84
|
+
- 不主动降级 (安全优先),L1 + high confidence + 有 evidence 且无测试失败 → L0 例外
|
|
85
85
|
|
|
86
86
|
### 11.6 — 处理 reviewer 结果
|
|
87
87
|
|
|
File without changes
|
|
File without changes
|