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.
Files changed (56) 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 +11 -4
  6. package/agents/executor.md +0 -0
  7. package/agents/researcher.md +0 -0
  8. package/agents/reviewer.md +12 -1
  9. package/commands/doctor.md +0 -1
  10. package/commands/prd.md +18 -1
  11. package/commands/resume.md +27 -21
  12. package/commands/start.md +8 -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 +72 -15
  17. package/hooks/gsd-context-monitor.cjs +0 -0
  18. package/hooks/gsd-session-init.cjs +0 -0
  19. package/hooks/gsd-session-stop.cjs +0 -0
  20. package/hooks/gsd-statusline.cjs +0 -0
  21. package/hooks/hooks.json +0 -0
  22. package/hooks/lib/gsd-finder.cjs +19 -3
  23. package/hooks/lib/semver-sort.cjs +20 -0
  24. package/install.js +84 -7
  25. package/launcher.js +0 -0
  26. package/package.json +1 -1
  27. package/references/anti-rationalization-full.md +0 -0
  28. package/references/evidence-spec.md +0 -0
  29. package/references/execution-loop.md +1 -1
  30. package/references/git-worktrees.md +0 -0
  31. package/references/questioning.md +0 -0
  32. package/references/review-classification.md +25 -6
  33. package/references/state-diagram.md +4 -3
  34. package/references/testing-patterns.md +0 -0
  35. package/src/schema.js +53 -30
  36. package/src/server.js +0 -0
  37. package/src/tools/orchestrator/debugger.js +0 -0
  38. package/src/tools/orchestrator/executor.js +10 -0
  39. package/src/tools/orchestrator/helpers.js +16 -9
  40. package/src/tools/orchestrator/index.js +0 -0
  41. package/src/tools/orchestrator/researcher.js +0 -0
  42. package/src/tools/orchestrator/resume.js +252 -188
  43. package/src/tools/orchestrator/reviewer.js +0 -0
  44. package/src/tools/state/constants.js +2 -0
  45. package/src/tools/state/crud.js +50 -44
  46. package/src/tools/state/index.js +0 -0
  47. package/src/tools/state/logic.js +0 -0
  48. package/src/tools/verify.js +0 -0
  49. package/src/utils.js +0 -0
  50. package/uninstall.js +12 -1
  51. package/workflows/debugging.md +14 -11
  52. package/workflows/deviation-rules.md +4 -4
  53. package/workflows/execution-flow.md +0 -0
  54. package/workflows/research.md +0 -0
  55. package/workflows/review-cycle.md +19 -2
  56. 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.5.14",
16
+ "version": "0.6.0",
17
17
  "keywords": [
18
18
  "orchestration",
19
19
  "mcp",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd",
3
- "version": "0.5.14",
3
+ "version": "0.6.0",
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
@@ -55,8 +55,11 @@ Phase 3 假设测试:
55
55
  3. 验证: 有效 → Phase 4 / 无效 → 新假设
56
56
 
57
57
  Phase 4 修复方向建议:
58
- 1. 提出修复方案 (针对根因,不是症状)
59
- 2. 建议失败测试用例 ( executor 实现)
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
  }
File without changes
File without changes
@@ -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>
@@ -102,4 +102,3 @@ Suggested Actions:
102
102
  - If a check cannot be performed (e.g., tool unavailable), report INFO rather than FAIL
103
103
  - Always show all 5 checks in the summary, even if some are INFO/skipped
104
104
  </rules>
105
- </output>
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
  - 直接作为需求描述使用
@@ -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
- 读取 `.gsd/state.json`:
15
- - 如果文件不存在 → 告知用户 "未找到 GSD 项目状态,请先运行 /gsd:start 或 /gsd:prd",停止
16
- - 如果文件损坏或解析失败告知用户并停止
14
+ 调用 MCP tool `orchestrator-resume`,使用响应中的 `summary` 字段展示状态给用户:
15
+ - 如果响应为 error 且 message 包含 "No .gsd directory" → 告知用户 "未找到 GSD 项目状态,请先运行 /gsd:start 或 /gsd:prd",停止
16
+ - 如果响应为 error 告知用户错误信息并停止
17
17
 
18
- 提取关键 canonical fields:
18
+ `summary` 字段包含:
19
19
  - `workflow_mode` — 当前工作流状态
20
- - `current_phase` / `current_task` 当前执行位置
21
- - `current_review` — 当前审查状态
22
- - `git_head` — 上次记录的 Git HEAD
23
- - `plan_version` — 计划版本号
24
- - `research.expires_at` — 研究过期时间
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
- - `current_phase` + `current_task` 恢复调度
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
- 项目: {project}
215
- 模式: {workflow_mode}
216
- 进度: Phase {current_phase}/{total_phases} | Task {done}/{tasks}
217
- 当前: {current_task} — {task_name}
218
- 下一步: {根据 workflow_mode 推导的下一动作}
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
- 所有展示数据从 canonical fields 实时推导,不使用 derived fields
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>
File without changes
package/commands/stop.md CHANGED
File without changes
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
- if (acquired) {
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
- function compareVersions(a, b) {
301
- const pa = a.split('.').map(Number);
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(pluginRoot, 'package.json.bak');
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
File without changes
package/hooks/hooks.json CHANGED
File without changes
@@ -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'))) return candidate;
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
- if (!settings.hooks) settings.hooks = {};
190
- const statuslinePath = join(CLAUDE_DIR, 'hooks', 'gsd-statusline.cjs');
191
- const statusLineRegistered = registerStatusLine(settings, statuslinePath);
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
- for (const config of HOOK_REGISTRY) {
194
- if (registerHookEntry(settings.hooks, config)) hooksRegistered = true;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-lite",
3
- "version": "0.5.14",
3
+ "version": "0.6.0",
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
@@ -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