gsd-lite 0.5.9 → 0.5.12

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 +33 -18
  5. package/agents/debugger.md +0 -0
  6. package/agents/executor.md +0 -0
  7. package/agents/researcher.md +24 -0
  8. package/agents/reviewer.md +0 -0
  9. package/commands/doctor.md +0 -0
  10. package/commands/prd.md +0 -0
  11. package/commands/resume.md +8 -0
  12. package/commands/start.md +0 -0
  13. package/commands/status.md +0 -0
  14. package/commands/stop.md +0 -0
  15. package/hooks/context-monitor.js +0 -0
  16. package/hooks/gsd-auto-update.cjs +0 -0
  17. package/hooks/gsd-context-monitor.cjs +0 -0
  18. package/hooks/gsd-session-init.cjs +104 -2
  19. package/hooks/gsd-session-stop.cjs +69 -0
  20. package/hooks/gsd-statusline.cjs +0 -0
  21. package/hooks/hooks.json +13 -1
  22. package/hooks/lib/gsd-finder.cjs +84 -0
  23. package/install.js +0 -0
  24. package/launcher.js +0 -0
  25. package/package.json +1 -1
  26. package/references/anti-rationalization-full.md +0 -0
  27. package/references/evidence-spec.md +3 -3
  28. package/references/execution-loop.md +0 -0
  29. package/references/git-worktrees.md +0 -0
  30. package/references/questioning.md +0 -0
  31. package/references/review-classification.md +1 -1
  32. package/references/state-diagram.md +1 -1
  33. package/references/testing-patterns.md +0 -0
  34. package/src/schema.js +8 -2
  35. package/src/server.js +32 -2
  36. package/src/tools/orchestrator/debugger.js +94 -0
  37. package/src/tools/orchestrator/executor.js +162 -0
  38. package/src/tools/orchestrator/helpers.js +447 -0
  39. package/src/tools/orchestrator/index.js +6 -0
  40. package/src/tools/orchestrator/researcher.js +27 -0
  41. package/src/tools/orchestrator/resume.js +457 -0
  42. package/src/tools/orchestrator/reviewer.js +125 -0
  43. package/src/tools/state/constants.js +67 -0
  44. package/src/tools/{state.js → state/crud.js} +279 -493
  45. package/src/tools/state/index.js +5 -0
  46. package/src/tools/state/logic.js +493 -0
  47. package/src/tools/verify.js +0 -0
  48. package/src/utils.js +0 -0
  49. package/uninstall.js +0 -0
  50. package/workflows/debugging.md +0 -0
  51. package/workflows/deviation-rules.md +0 -0
  52. package/workflows/execution-flow.md +0 -0
  53. package/workflows/research.md +0 -0
  54. package/workflows/review-cycle.md +0 -0
  55. package/workflows/tdd-cycle.md +0 -0
  56. package/src/tools/orchestrator.js +0 -1242
@@ -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.9",
16
+ "version": "0.5.12",
17
17
  "keywords": [
18
18
  "orchestration",
19
19
  "mcp",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd",
3
- "version": "0.5.9",
3
+ "version": "0.5.12",
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
@@ -10,9 +10,9 @@ GSD-Lite is an AI orchestration tool for [Claude Code](https://docs.anthropic.co
10
10
 
11
11
  ### Structured Execution Engine
12
12
  - **Phase-based project management** — Break work into phases with ordered tasks, dependency tracking, and handoff gates
13
- - **State machine orchestration** — 11 workflow modes with precise state transitions, persistent to `state.json`
13
+ - **State machine orchestration** — 12 workflow modes with precise state transitions, persistent to `state.json`
14
14
  - **Automatic task scheduling** — Gate-aware dependency resolution determines what runs next
15
- - **Session resilience** — Stop anytime, resume exactly where you left off — even across Claude Code restarts
15
+ - **Session resilience** — Stop anytime, resume exactly where you left off — crash protection via Stop hook auto-saves state markers
16
16
 
17
17
  ### Quality Discipline (Built-in, Not Optional)
18
18
  - **TDD enforcement** — "No production code without a failing test first" baked into every executor dispatch
@@ -29,6 +29,7 @@ GSD-Lite is an AI orchestration tool for [Claude Code](https://docs.anthropic.co
29
29
  ### Context Protection
30
30
  - **Subagent isolation** — Each task runs in its own agent context, preventing cross-contamination
31
31
  - **StatusLine monitoring** — Real-time context health tracking via Claude Code StatusLine
32
+ - **Session lifecycle hooks** — Stop hook writes crash marker; SessionStart injects project status into CLAUDE.md; resume detects non-graceful exits
32
33
  - **Evidence-based verification** — Every claim backed by command output, not assertions
33
34
  - **Research with TTL** — Research artifacts include volatility ratings and expiration dates
34
35
 
@@ -41,7 +42,7 @@ User → discuss + research (confirm requirements) → approve plan → auto-exe
41
42
  (code→review→verify→advance)
42
43
  ```
43
44
 
44
- ### 5 Commands
45
+ ### 6 Commands
45
46
 
46
47
  | Command | Purpose |
47
48
  |---------|---------|
@@ -50,6 +51,7 @@ User → discuss + research (confirm requirements) → approve plan → auto-exe
50
51
  | `/gsd:resume` | Resume execution from saved state |
51
52
  | `/gsd:status` | View project progress dashboard |
52
53
  | `/gsd:stop` | Save state and pause execution |
54
+ | `/gsd:doctor` | Diagnostic checks on GSD-Lite installation and project health |
53
55
 
54
56
  ### 4 Agents
55
57
 
@@ -60,7 +62,7 @@ User → discuss + research (confirm requirements) → approve plan → auto-exe
60
62
  | **researcher** | Ecosystem research (Context7 → official docs → web) | Confidence scoring + TTL |
61
63
  | **debugger** | 4-phase systematic root cause analysis | Root Cause Iron Law |
62
64
 
63
- ### MCP Server (10 Tools)
65
+ ### MCP Server (11 Tools)
64
66
 
65
67
  | Tool | Purpose |
66
68
  |------|---------|
@@ -68,6 +70,7 @@ User → discuss + research (confirm requirements) → approve plan → auto-exe
68
70
  | `state-init` | Initialize `.gsd/` directory with project structure |
69
71
  | `state-read` | Read state with optional field filtering |
70
72
  | `state-update` | Update canonical fields with lifecycle validation |
73
+ | `state-patch` | Incrementally modify plan (add/remove/reorder tasks, update fields, add dependencies) |
71
74
  | `phase-complete` | Complete a phase after verifying handoff gates |
72
75
  | `orchestrator-resume` | Resume orchestration from current state |
73
76
  | `orchestrator-handle-executor-result` | Process executor output, advance lifecycle |
@@ -202,33 +205,45 @@ All state lives in `.gsd/state.json` — a single source of truth with:
202
205
 
203
206
  | Dimension | GSD | GSD-Lite |
204
207
  |-----------|-----|----------|
205
- | Commands | 32 | **5** |
208
+ | Commands | 32 | **6** |
206
209
  | Agents | 12 | **4** |
207
210
  | Source files | 100+ | **~35** |
208
211
  | Installer | 2465 lines | **~80 lines** |
209
212
  | User interactions | 6+ confirmations | **Typically 2** |
210
213
  | TDD / Anti-rationalization | No | **Yes** |
211
- | State machine recovery | Partial | **Full (11 modes)** |
214
+ | State machine recovery | Partial | **Full (12 modes)** |
212
215
  | Evidence-based verification | No | **Yes** |
213
216
 
214
217
  ## Project Structure
215
218
 
216
219
  ```
217
220
  gsd-lite/
218
- ├── src/ # MCP Server + tools (~3300 lines)
219
- │ ├── server.js # MCP Server entry (10 tools)
221
+ ├── src/ # MCP Server + tools
222
+ │ ├── server.js # MCP Server entry (11 tools)
220
223
  │ ├── schema.js # State schema + lifecycle validation
221
- │ ├── utils.js # Shared utilities (atomic writes, git)
224
+ │ ├── utils.js # Shared utilities (atomic writes, git, file lock)
222
225
  │ └── tools/
223
- │ ├── state.js # State CRUD + evidence + propagation
224
- │ ├── orchestrator.js # Orchestration logic + agent handlers
226
+ │ ├── state/ # State management (modular)
227
+ ├── constants.js # Error codes, lock infrastructure
228
+ │ │ ├── crud.js # CRUD operations + plan patching
229
+ │ │ ├── logic.js # Task scheduling, propagation, research
230
+ │ │ └── index.js # Re-exports
231
+ │ ├── orchestrator/ # Orchestration logic (modular)
232
+ │ │ ├── helpers.js # Shared constants, preflight, dispatch
233
+ │ │ ├── resume.js # Workflow resume state machine
234
+ │ │ ├── executor.js # Executor result handler
235
+ │ │ ├── reviewer.js # Reviewer result handler
236
+ │ │ ├── debugger.js # Debugger result handler
237
+ │ │ ├── researcher.js # Researcher result handler
238
+ │ │ └── index.js # Re-exports
225
239
  │ └── verify.js # lint/typecheck/test verification
226
- ├── commands/ # 5 slash commands
227
- ├── agents/ # 4 subagent prompts
228
- ├── workflows/ # 5 core workflows (TDD, review, debug, research, deviation)
229
- ├── references/ # 8 reference docs (execution loop, state diagram, evidence spec, ...)
230
- ├── hooks/ # Context monitoring (StatusLine + PostToolUse)
231
- ├── tests/ # 674 tests (unit + simulation + E2E)
240
+ ├── commands/ # 6 slash commands (start, prd, resume, status, stop, doctor)
241
+ ├── agents/ # 4 subagent prompts (executor, reviewer, researcher, debugger)
242
+ ├── workflows/ # 6 core workflows (TDD, review, debug, research, deviation, execution-flow)
243
+ ├── references/ # 8 reference docs
244
+ ├── hooks/ # Session lifecycle (StatusLine + PostToolUse + SessionStart + Stop + AutoUpdate)
245
+ │ └── lib/ # Shared hook utilities (gsd-finder)
246
+ ├── tests/ # 804 tests (unit + simulation + E2E)
232
247
  ├── cli.js # Install/uninstall CLI entry
233
248
  ├── install.js # Installation script
234
249
  └── uninstall.js # Uninstall script
@@ -237,7 +252,7 @@ gsd-lite/
237
252
  ## Testing
238
253
 
239
254
  ```bash
240
- npm test # Run all 674 tests
255
+ npm test # Run all 804 tests
241
256
  npm run test:coverage # Tests + coverage report (94%+ lines, 81%+ branches)
242
257
  npm run lint # Biome lint
243
258
  node --test tests/file.js # Run a single test file
File without changes
File without changes
@@ -27,6 +27,9 @@ tools: Read, Write, Bash, WebSearch, WebFetch, mcp__plugin_context7_context7__*
27
27
  关键推荐生成 decision id,供 plan/task 的 `research_basis` 引用
28
28
 
29
29
  <result_contract>
30
+ 编排器调用 `orchestrator-handle-researcher-result` 需要三个参数:
31
+
32
+ **1. result** — 研究元数据:
30
33
  ```json
31
34
  {
32
35
  "decision_ids": ["decision:jwt-rotation"],
@@ -37,6 +40,27 @@ tools: Read, Write, Bash, WebSearch, WebFetch, mcp__plugin_context7_context7__*
37
40
  ]
38
41
  }
39
42
  ```
43
+
44
+ **2. decision_index** — 以 decision id 为 key 的索引对象 (每个 decision_ids 中的 id 必须在此出现):
45
+ ```json
46
+ {
47
+ "decision:jwt-rotation": {
48
+ "summary": "Use refresh token rotation for JWT auth",
49
+ "source": "Context7",
50
+ "expires_at": "2026-03-16T10:30:00Z"
51
+ }
52
+ }
53
+ ```
54
+
55
+ **3. artifacts** — 四个研究文档的 Markdown 内容 (上方 research_output 中的四个文件):
56
+ ```json
57
+ {
58
+ "STACK.md": "# 技术栈推荐\n...",
59
+ "ARCHITECTURE.md": "# 架构模式\n...",
60
+ "PITFALLS.md": "# 领域陷阱\n...",
61
+ "SUMMARY.md": "# 摘要\n..."
62
+ }
63
+ ```
40
64
  </result_contract>
41
65
  </research_output>
42
66
 
File without changes
File without changes
package/commands/prd.md CHANGED
File without changes
@@ -28,6 +28,14 @@ description: Resume project execution from saved state with workspace validation
28
28
  <HARD-GATE id="resume-preflight">
29
29
  必须在恢复执行前完成所有校验,按以下优先级顺序:
30
30
 
31
+ 0. **Session End 检查:**
32
+ - 检查 `.gsd/.session-end` 文件是否存在
33
+ - 如果存在:
34
+ - 读取内容,向用户展示: "⚠️ 上次 session 在 {ended_at} 非正常结束,当时处于 {workflow_mode_was} (Phase {current_phase} / Task {current_task})"
35
+ - 删除 `.session-end` 文件
36
+ - 继续后续校验 (不覆写 workflow_mode — 由下面的校验决定)
37
+ - 如果不存在 → 跳过,继续后续校验
38
+
31
39
  1. **Git HEAD 校验:**
32
40
  - 运行 `git rev-parse HEAD` 获取当前 HEAD
33
41
  - 如果与 state.json 中的 `git_head` 不同:
package/commands/start.md CHANGED
File without changes
File without changes
package/commands/stop.md CHANGED
File without changes
File without changes
File without changes
File without changes
@@ -2,8 +2,10 @@
2
2
  // GSD-Lite SessionStart hook
3
3
  // 1. Cleans up stale temp files (throttled to once/day).
4
4
  // 2. Auto-registers statusLine in settings.json if not already configured.
5
- // 3. Shows notification if a previous background update completed or found a new version.
6
- // 4. Spawns background auto-update (detached, non-blocking).
5
+ // 3. Self-heals .mcp.json if missing.
6
+ // 4. Shows notification if a previous background update completed or found a new version.
7
+ // 5. Spawns background auto-update (detached, non-blocking).
8
+ // 6. Injects GSD project progress into stdout + CLAUDE.md (if active project found).
7
9
  // Idempotent: skips if statusLine already points to gsd-statusline, preserves
8
10
  // third-party statuslines.
9
11
 
@@ -124,4 +126,104 @@ setTimeout(() => process.exit(0), 4000).unref();
124
126
  );
125
127
  child.unref();
126
128
  } catch { /* silent — never block session start */ }
129
+
130
+ // ── Phase 6: GSD Project Progress Injection ──
131
+ // If an active GSD project exists, inject progress into stdout (additionalContext)
132
+ // and write a status block into CLAUDE.md for persistent visibility.
133
+ try {
134
+ const { findGsdDir, readState, getProgress } = require('./lib/gsd-finder.cjs');
135
+ const cwd = process.cwd();
136
+ const gsdDir = findGsdDir(cwd);
137
+ if (gsdDir) {
138
+ const state = readState(gsdDir);
139
+ const progress = getProgress(state);
140
+ if (progress) {
141
+ // Check for .session-end marker (previous non-graceful exit)
142
+ const markerPath = path.join(gsdDir, '.session-end');
143
+ let sessionEndInfo = null;
144
+ try {
145
+ if (fs.existsSync(markerPath)) {
146
+ sessionEndInfo = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
147
+ }
148
+ } catch { /* skip */ }
149
+
150
+ // Stdout: only output session-end warning (crash recovery), skip routine progress
151
+ // Routine progress is handled by CLAUDE.md injection below — avoids noise
152
+ const shortHead = progress.gitHead ? progress.gitHead.substring(0, 7) : 'n/a';
153
+ if (sessionEndInfo) {
154
+ console.log(`⚠️ GSD: Previous session ended unexpectedly at ${sessionEndInfo.ended_at} (was: ${sessionEndInfo.workflow_mode_was}). Run /gsd:resume to recover.`);
155
+ }
156
+
157
+ // Write status block to CLAUDE.md
158
+ const projectRoot = path.dirname(gsdDir);
159
+ const claudeMdPath = path.join(projectRoot, 'CLAUDE.md');
160
+ const BEGIN_MARKER = '<!-- GSD-STATUS-BEGIN -->';
161
+ const END_MARKER = '<!-- GSD-STATUS-END -->';
162
+
163
+ const statusBlock = [
164
+ BEGIN_MARKER,
165
+ `### GSD Project: ${progress.project}`,
166
+ `- Phase: ${progress.currentPhase || '?'}/${progress.totalPhases} (${progress.phaseName})`,
167
+ `- Task: ${progress.currentTask || 'none'}${progress.taskName ? ` (${progress.taskName})` : ''}`,
168
+ `- Mode: ${progress.workflowMode}`,
169
+ `- Progress: ${progress.acceptedTasks}/${progress.totalTasks} tasks done`,
170
+ `- Last checkpoint: ${shortHead}`,
171
+ sessionEndInfo ? `- ⚠️ Previous session ended unexpectedly (${sessionEndInfo.ended_at})` : null,
172
+ END_MARKER,
173
+ ].filter(Boolean).join('\n');
174
+
175
+ try {
176
+ let content = '';
177
+ try {
178
+ content = fs.readFileSync(claudeMdPath, 'utf8');
179
+ } catch { /* file doesn't exist yet — will create */ }
180
+
181
+ const beginIdx = content.indexOf(BEGIN_MARKER);
182
+ const endIdx = content.indexOf(END_MARKER);
183
+
184
+ let newContent;
185
+ if (beginIdx !== -1 && endIdx !== -1) {
186
+ // Replace existing block
187
+ newContent = content.substring(0, beginIdx) + statusBlock + content.substring(endIdx + END_MARKER.length);
188
+ } else {
189
+ // Append to end (with blank line separator)
190
+ const separator = content.length > 0 && !content.endsWith('\n\n') ? (content.endsWith('\n') ? '\n' : '\n\n') : '';
191
+ newContent = content + separator + statusBlock + '\n';
192
+ }
193
+
194
+ // Only write if content changed
195
+ if (newContent !== content) {
196
+ const tmpClaude = claudeMdPath + `.gsd-tmp-${process.pid}`;
197
+ fs.writeFileSync(tmpClaude, newContent);
198
+ fs.renameSync(tmpClaude, claudeMdPath);
199
+ }
200
+ } catch (e) {
201
+ if (process.env.GSD_DEBUG) process.stderr.write(`gsd-session-init: CLAUDE.md write failed: ${e.message}\n`);
202
+ }
203
+ }
204
+ } else {
205
+ // No active GSD project — clean up stale CLAUDE.md block if it exists
206
+ try {
207
+ const claudeMdPath = path.join(cwd, 'CLAUDE.md');
208
+ const BEGIN_MARKER = '<!-- GSD-STATUS-BEGIN -->';
209
+ const END_MARKER = '<!-- GSD-STATUS-END -->';
210
+ const content = fs.readFileSync(claudeMdPath, 'utf8');
211
+ const beginIdx = content.indexOf(BEGIN_MARKER);
212
+ const endIdx = content.indexOf(END_MARKER);
213
+ if (beginIdx !== -1 && endIdx !== -1) {
214
+ // Remove the block and any trailing newline
215
+ let newContent = content.substring(0, beginIdx) + content.substring(endIdx + END_MARKER.length);
216
+ // Clean up extra blank lines left behind
217
+ newContent = newContent.replace(/\n{3,}/g, '\n\n').trimEnd() + '\n';
218
+ if (newContent !== content) {
219
+ const tmpClaude = claudeMdPath + `.gsd-tmp-${process.pid}`;
220
+ fs.writeFileSync(tmpClaude, newContent);
221
+ fs.renameSync(tmpClaude, claudeMdPath);
222
+ }
223
+ }
224
+ } catch { /* no CLAUDE.md or no block to clean — skip */ }
225
+ }
226
+ } catch (e) {
227
+ if (process.env.GSD_DEBUG) process.stderr.write(`gsd-session-init phase 6: ${e.message}\n`);
228
+ }
127
229
  })().catch(() => {});
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ // GSD-Lite Stop hook — Crash Protection
3
+ //
4
+ // Runs when Claude Code session ends (exit, /clear, crash).
5
+ // If an active GSD project is found, writes a .session-end marker file
6
+ // so that /gsd:resume can detect the non-graceful exit and inform the user.
7
+ //
8
+ // Design decisions:
9
+ // - Does NOT modify state.json directly (avoids bypassing schema validation)
10
+ // - Uses a marker file (.gsd/.session-end) that resume preflight checks
11
+ // - Only acts on active sessions (not completed/failed/paused)
12
+ // - Timeout guard: exits after 4s (hook timeout is 5s)
13
+
14
+ 'use strict';
15
+
16
+ const fs = require('node:fs');
17
+ const path = require('node:path');
18
+ const { findGsdDir, readState } = require('./lib/gsd-finder.cjs');
19
+
20
+ // Safety: exit after 4s regardless
21
+ setTimeout(() => process.exit(0), 4000).unref();
22
+
23
+ const TERMINAL_MODES = ['completed', 'failed', 'paused_by_user'];
24
+
25
+ (async () => {
26
+ const cwd = process.cwd();
27
+ const gsdDir = findGsdDir(cwd);
28
+ if (!gsdDir) process.exit(0);
29
+
30
+ const state = readState(gsdDir);
31
+ if (!state) process.exit(0);
32
+
33
+ // Only write marker for active (non-terminal, non-paused) sessions
34
+ if (TERMINAL_MODES.includes(state.workflow_mode)) process.exit(0);
35
+
36
+ // Get current git HEAD
37
+ let gitHead = state.git_head || '';
38
+ try {
39
+ const { execSync } = require('node:child_process');
40
+ gitHead = execSync('git rev-parse HEAD', {
41
+ cwd: path.dirname(gsdDir),
42
+ timeout: 2000,
43
+ encoding: 'utf8',
44
+ }).trim();
45
+ } catch { /* keep existing git_head */ }
46
+
47
+ // Write .session-end marker
48
+ const marker = {
49
+ ended_at: new Date().toISOString(),
50
+ workflow_mode_was: state.workflow_mode,
51
+ current_phase: state.current_phase,
52
+ current_task: state.current_task,
53
+ git_head: gitHead,
54
+ reason: 'session_stop',
55
+ };
56
+
57
+ const markerPath = path.join(gsdDir, '.session-end');
58
+ const tmpPath = markerPath + `.${process.pid}.tmp`;
59
+ try {
60
+ fs.writeFileSync(tmpPath, JSON.stringify(marker, null, 2) + '\n');
61
+ fs.renameSync(tmpPath, markerPath);
62
+ } catch (e) {
63
+ // Clean up tmp if rename failed
64
+ try { fs.unlinkSync(tmpPath); } catch { /* ignore */ }
65
+ if (process.env.GSD_DEBUG) {
66
+ process.stderr.write(`gsd-session-stop: ${e.message}\n`);
67
+ }
68
+ }
69
+ })().catch(() => {});
File without changes
package/hooks/hooks.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "GSD-Lite hooks: statusline auto-registration + context health monitor",
2
+ "description": "GSD-Lite hooks: statusline + context monitor + session lifecycle",
3
3
  "hooks": {
4
4
  "SessionStart": [
5
5
  {
@@ -24,6 +24,18 @@
24
24
  }
25
25
  ]
26
26
  }
27
+ ],
28
+ "Stop": [
29
+ {
30
+ "matcher": "*",
31
+ "hooks": [
32
+ {
33
+ "type": "command",
34
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-session-stop.cjs\"",
35
+ "timeout": 5
36
+ }
37
+ ]
38
+ }
27
39
  ]
28
40
  }
29
41
  }
@@ -0,0 +1,84 @@
1
+ #!/usr/bin/env node
2
+ // Shared utilities for GSD hooks.
3
+ // findGsdDir: walk up from startDir looking for .gsd/state.json
4
+ // readState: parse .gsd/state.json, return null on failure
5
+ // getProgress: compute progress summary from state
6
+
7
+ 'use strict';
8
+
9
+ const fs = require('node:fs');
10
+ const path = require('node:path');
11
+
12
+ /**
13
+ * Walk from startDir up to filesystem root looking for a .gsd directory
14
+ * that contains state.json. Returns the absolute path to .gsd or null.
15
+ */
16
+ function findGsdDir(startDir) {
17
+ let dir = startDir;
18
+ while (true) {
19
+ const candidate = path.join(dir, '.gsd');
20
+ try {
21
+ if (fs.statSync(candidate).isDirectory()) {
22
+ // Only return if state.json exists (not just an empty .gsd dir)
23
+ if (fs.existsSync(path.join(candidate, 'state.json'))) return candidate;
24
+ }
25
+ } catch { /* skip */ }
26
+ const parent = path.dirname(dir);
27
+ if (parent === dir) return null;
28
+ dir = parent;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Read and parse .gsd/state.json. Returns parsed object or null on any failure.
34
+ */
35
+ function readState(gsdDir) {
36
+ try {
37
+ return JSON.parse(fs.readFileSync(path.join(gsdDir, 'state.json'), 'utf8'));
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Compute progress summary from state object.
45
+ * Returns { project, workflowMode, currentPhase, totalPhases, currentTask,
46
+ * phaseName, taskName, acceptedTasks, totalTasks, gitHead }
47
+ */
48
+ function getProgress(state) {
49
+ if (!state) return null;
50
+
51
+ const phases = state.phases || [];
52
+ let acceptedTasks = 0;
53
+ let totalTasks = 0;
54
+ let phaseName = '';
55
+ let taskName = '';
56
+
57
+ for (const phase of phases) {
58
+ const todos = phase.todo || [];
59
+ totalTasks += todos.length;
60
+ acceptedTasks += todos.filter(t => t.lifecycle === 'accepted').length;
61
+ if (phase.id === state.current_phase) {
62
+ phaseName = phase.name || `Phase ${phase.id}`;
63
+ const task = todos.find(t => t.id === state.current_task);
64
+ if (task) {
65
+ taskName = task.name || '';
66
+ }
67
+ }
68
+ }
69
+
70
+ return {
71
+ project: state.project || 'Unknown',
72
+ workflowMode: state.workflow_mode || 'unknown',
73
+ currentPhase: state.current_phase,
74
+ totalPhases: state.total_phases || phases.length,
75
+ currentTask: state.current_task,
76
+ phaseName,
77
+ taskName,
78
+ acceptedTasks,
79
+ totalTasks,
80
+ gitHead: state.git_head || '',
81
+ };
82
+ }
83
+
84
+ module.exports = { findGsdDir, readState, getProgress };
package/install.js CHANGED
File without changes
package/launcher.js CHANGED
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-lite",
3
- "version": "0.5.9",
3
+ "version": "0.5.12",
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
@@ -79,14 +79,14 @@ task:3.1 -> phase 3, task 1
79
79
 
80
80
  此函数用于 evidence 归档时判断 evidence 所属 phase。
81
81
 
82
- 来源: `parseScopePhase()` in `src/tools/state.js`
82
+ 来源: `parseScopePhase()` in `src/tools/state/`
83
83
 
84
84
  ## 容量限制与自动裁剪
85
85
 
86
86
  ### MAX_EVIDENCE_ENTRIES
87
87
 
88
88
  - 硬限制: `200` 条
89
- - 定义位置: `src/tools/state.js` 顶层常量
89
+ - 定义位置: `src/tools/state/` 顶层常量
90
90
 
91
91
  ### 自动裁剪触发
92
92
 
@@ -163,4 +163,4 @@ _pruneEvidenceFromState()
163
163
  - 更新时机: executor checkpointed / blocked / failed 时从 result.evidence 覆写
164
164
  - 清空时机: `propagateInvalidation()` 或 reviewer 标记 rework 时清空为 `[]`
165
165
 
166
- 来源: `addEvidence()`, `_pruneEvidenceFromState()`, `pruneEvidence()`, `phaseComplete()` in `src/tools/state.js`; `handleExecutorResult()`, `handleReviewerResult()` in `src/tools/orchestrator.js`
166
+ 来源: `addEvidence()`, `_pruneEvidenceFromState()`, `pruneEvidence()`, `phaseComplete()` in `src/tools/state/`; `handleExecutorResult()`, `handleReviewerResult()` in `src/tools/orchestrator.js`
File without changes
File without changes
File without changes
@@ -34,7 +34,7 @@ task.level 当前值?
34
34
  └── 否 -> 保持当前级别
35
35
  ```
36
36
 
37
- 来源: `reclassifyReviewLevel()` in `src/tools/state.js`
37
+ 来源: `reclassifyReviewLevel()` in `src/tools/state/`
38
38
 
39
39
  ## 审查流程
40
40
 
@@ -215,4 +215,4 @@ stateDiagram-v2
215
215
  **Research 刷新后恢复**:
216
216
  `storeResearch()` 中: 如果 `workflow_mode === 'research_refresh_needed'`,调用 `inferWorkflowModeAfterResearch()` 根据 `current_review` 状态推断恢复到 `reviewing_phase` / `reviewing_task` / `executing_task`。
217
217
 
218
- 来源: `WORKFLOW_MODES` in `src/schema.js`, `resumeWorkflow()`, `evaluatePreflight()` in `src/tools/orchestrator.js`, `storeResearch()` in `src/tools/state.js`
218
+ 来源: `WORKFLOW_MODES` in `src/schema.js`, `resumeWorkflow()`, `evaluatePreflight()` in `src/tools/orchestrator.js`, `storeResearch()` in `src/tools/state/`
File without changes
package/src/schema.js CHANGED
@@ -595,8 +595,8 @@ export function validateExecutorResult(r) {
595
595
  export function validateReviewerResult(r) {
596
596
  const errors = [];
597
597
  if (!['task', 'phase'].includes(r.scope)) errors.push('invalid scope');
598
- if (!(typeof r.scope_id === 'string' || typeof r.scope_id === 'number') || r.scope_id === '') {
599
- errors.push('missing scope_id');
598
+ if (!(typeof r.scope_id === 'string' || typeof r.scope_id === 'number') || r.scope_id === '' || r.scope_id === 0) {
599
+ errors.push('missing or invalid scope_id');
600
600
  }
601
601
  if (!['L2', 'L1-batch', 'L1'].includes(r.review_level)) errors.push('invalid review_level (expected L2, L1-batch, or L1)');
602
602
  if (typeof r.spec_passed !== 'boolean') errors.push('spec_passed must be boolean');
@@ -712,6 +712,8 @@ export function createInitialState({ project, phases }) {
712
712
  if (!Array.isArray(phases)) {
713
713
  return { error: true, message: 'phases must be an array' };
714
714
  }
715
+ // Note: empty phases is allowed here for internal/test use;
716
+ // the public API guard is in init() which rejects phases.length === 0.
715
717
  // Validate task names and uniqueness before creating state
716
718
  const seenIds = new Set();
717
719
  for (const [pi, p] of phases.entries()) {
@@ -741,6 +743,10 @@ export function createInitialState({ project, phases }) {
741
743
  if (!['task', 'phase'].includes(dep.kind)) {
742
744
  return { error: true, message: `Task ${taskId}: requires entry kind must be "task" or "phase" (got "${dep.kind}")` };
743
745
  }
746
+ const validGates = ['checkpoint', 'accepted', 'phase_complete'];
747
+ if (dep.gate && !validGates.includes(dep.gate)) {
748
+ return { error: true, message: `Task ${taskId}: requires entry gate must be one of ${validGates.join(', ')} (got "${dep.gate}")` };
749
+ }
744
750
  if (dep.kind === 'task' && !seenIds.has(String(dep.id))) {
745
751
  return { error: true, message: `Task ${taskId}: requires references non-existent task "${dep.id}" (valid IDs: ${[...seenIds].join(', ')})` };
746
752
  }