gsd-lite 0.5.10 → 0.5.13
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/README.md +39 -18
- package/agents/executor.md +8 -0
- package/agents/researcher.md +24 -0
- package/agents/reviewer.md +14 -0
- package/commands/resume.md +8 -0
- package/hooks/gsd-session-init.cjs +104 -2
- package/hooks/gsd-session-stop.cjs +69 -0
- package/hooks/hooks.json +13 -1
- package/hooks/lib/gsd-finder.cjs +84 -0
- package/package.json +1 -1
- package/references/evidence-spec.md +3 -3
- package/references/review-classification.md +1 -1
- package/references/state-diagram.md +1 -1
- package/src/schema.js +12 -2
- package/src/server.js +31 -2
- package/src/tools/orchestrator/debugger.js +94 -0
- package/src/tools/orchestrator/executor.js +162 -0
- package/src/tools/orchestrator/helpers.js +448 -0
- package/src/tools/orchestrator/index.js +6 -0
- package/src/tools/orchestrator/researcher.js +27 -0
- package/src/tools/orchestrator/resume.js +478 -0
- package/src/tools/orchestrator/reviewer.js +125 -0
- package/src/tools/state/constants.js +67 -0
- package/src/tools/{state.js → state/crud.js} +276 -493
- package/src/tools/state/index.js +5 -0
- package/src/tools/state/logic.js +508 -0
- package/src/tools/orchestrator.js +0 -1243
|
@@ -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.
|
|
16
|
+
"version": "0.5.13",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
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** —
|
|
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 —
|
|
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
|
|
@@ -26,9 +26,16 @@ GSD-Lite is an AI orchestration tool for [Claude Code](https://docs.anthropic.co
|
|
|
26
26
|
- **Blocked task handling** — Blocked tasks are parked; execution continues with remaining tasks
|
|
27
27
|
- **Rework propagation** — Critical review issues cascade invalidation to dependent tasks
|
|
28
28
|
|
|
29
|
+
### Adaptive Review & Parallel Execution
|
|
30
|
+
- **Confidence-based review adjustment** — Executor self-assesses confidence (high/medium/low); orchestrator auto-adjusts review level accordingly
|
|
31
|
+
- **Impact analysis before review** — Reviewer runs impact analysis on multi-file changes to catch missed downstream effects
|
|
32
|
+
- **Parallel task scheduling** — Independent tasks within the same phase are identified for concurrent dispatch
|
|
33
|
+
- **Auto PR suggestion** — Phase/project completion prompts PR creation with evidence summary
|
|
34
|
+
|
|
29
35
|
### Context Protection
|
|
30
36
|
- **Subagent isolation** — Each task runs in its own agent context, preventing cross-contamination
|
|
31
37
|
- **StatusLine monitoring** — Real-time context health tracking via Claude Code StatusLine
|
|
38
|
+
- **Session lifecycle hooks** — Stop hook writes crash marker; SessionStart injects project status into CLAUDE.md; resume detects non-graceful exits
|
|
32
39
|
- **Evidence-based verification** — Every claim backed by command output, not assertions
|
|
33
40
|
- **Research with TTL** — Research artifacts include volatility ratings and expiration dates
|
|
34
41
|
|
|
@@ -41,7 +48,7 @@ User → discuss + research (confirm requirements) → approve plan → auto-exe
|
|
|
41
48
|
(code→review→verify→advance)
|
|
42
49
|
```
|
|
43
50
|
|
|
44
|
-
###
|
|
51
|
+
### 6 Commands
|
|
45
52
|
|
|
46
53
|
| Command | Purpose |
|
|
47
54
|
|---------|---------|
|
|
@@ -50,6 +57,7 @@ User → discuss + research (confirm requirements) → approve plan → auto-exe
|
|
|
50
57
|
| `/gsd:resume` | Resume execution from saved state |
|
|
51
58
|
| `/gsd:status` | View project progress dashboard |
|
|
52
59
|
| `/gsd:stop` | Save state and pause execution |
|
|
60
|
+
| `/gsd:doctor` | Diagnostic checks on GSD-Lite installation and project health |
|
|
53
61
|
|
|
54
62
|
### 4 Agents
|
|
55
63
|
|
|
@@ -60,7 +68,7 @@ User → discuss + research (confirm requirements) → approve plan → auto-exe
|
|
|
60
68
|
| **researcher** | Ecosystem research (Context7 → official docs → web) | Confidence scoring + TTL |
|
|
61
69
|
| **debugger** | 4-phase systematic root cause analysis | Root Cause Iron Law |
|
|
62
70
|
|
|
63
|
-
### MCP Server (
|
|
71
|
+
### MCP Server (11 Tools)
|
|
64
72
|
|
|
65
73
|
| Tool | Purpose |
|
|
66
74
|
|------|---------|
|
|
@@ -68,6 +76,7 @@ User → discuss + research (confirm requirements) → approve plan → auto-exe
|
|
|
68
76
|
| `state-init` | Initialize `.gsd/` directory with project structure |
|
|
69
77
|
| `state-read` | Read state with optional field filtering |
|
|
70
78
|
| `state-update` | Update canonical fields with lifecycle validation |
|
|
79
|
+
| `state-patch` | Incrementally modify plan (add/remove/reorder tasks, update fields, add dependencies) |
|
|
71
80
|
| `phase-complete` | Complete a phase after verifying handoff gates |
|
|
72
81
|
| `orchestrator-resume` | Resume orchestration from current state |
|
|
73
82
|
| `orchestrator-handle-executor-result` | Process executor output, advance lifecycle |
|
|
@@ -202,33 +211,45 @@ All state lives in `.gsd/state.json` — a single source of truth with:
|
|
|
202
211
|
|
|
203
212
|
| Dimension | GSD | GSD-Lite |
|
|
204
213
|
|-----------|-----|----------|
|
|
205
|
-
| Commands | 32 | **
|
|
214
|
+
| Commands | 32 | **6** |
|
|
206
215
|
| Agents | 12 | **4** |
|
|
207
216
|
| Source files | 100+ | **~35** |
|
|
208
217
|
| Installer | 2465 lines | **~80 lines** |
|
|
209
218
|
| User interactions | 6+ confirmations | **Typically 2** |
|
|
210
219
|
| TDD / Anti-rationalization | No | **Yes** |
|
|
211
|
-
| State machine recovery | Partial | **Full (
|
|
220
|
+
| State machine recovery | Partial | **Full (12 modes)** |
|
|
212
221
|
| Evidence-based verification | No | **Yes** |
|
|
213
222
|
|
|
214
223
|
## Project Structure
|
|
215
224
|
|
|
216
225
|
```
|
|
217
226
|
gsd-lite/
|
|
218
|
-
├── src/ # MCP Server + tools
|
|
219
|
-
│ ├── server.js # MCP Server entry (
|
|
227
|
+
├── src/ # MCP Server + tools
|
|
228
|
+
│ ├── server.js # MCP Server entry (11 tools)
|
|
220
229
|
│ ├── schema.js # State schema + lifecycle validation
|
|
221
|
-
│ ├── utils.js # Shared utilities (atomic writes, git)
|
|
230
|
+
│ ├── utils.js # Shared utilities (atomic writes, git, file lock)
|
|
222
231
|
│ └── tools/
|
|
223
|
-
│ ├── state
|
|
224
|
-
│ ├──
|
|
232
|
+
│ ├── state/ # State management (modular)
|
|
233
|
+
│ │ ├── constants.js # Error codes, lock infrastructure
|
|
234
|
+
│ │ ├── crud.js # CRUD operations + plan patching
|
|
235
|
+
│ │ ├── logic.js # Task scheduling, propagation, research
|
|
236
|
+
│ │ └── index.js # Re-exports
|
|
237
|
+
│ ├── orchestrator/ # Orchestration logic (modular)
|
|
238
|
+
│ │ ├── helpers.js # Shared constants, preflight, dispatch
|
|
239
|
+
│ │ ├── resume.js # Workflow resume state machine
|
|
240
|
+
│ │ ├── executor.js # Executor result handler
|
|
241
|
+
│ │ ├── reviewer.js # Reviewer result handler
|
|
242
|
+
│ │ ├── debugger.js # Debugger result handler
|
|
243
|
+
│ │ ├── researcher.js # Researcher result handler
|
|
244
|
+
│ │ └── index.js # Re-exports
|
|
225
245
|
│ └── verify.js # lint/typecheck/test verification
|
|
226
|
-
├── commands/ #
|
|
227
|
-
├── agents/ # 4 subagent prompts
|
|
228
|
-
├── workflows/ #
|
|
229
|
-
├── references/ # 8 reference docs
|
|
230
|
-
├── hooks/ #
|
|
231
|
-
|
|
246
|
+
├── commands/ # 6 slash commands (start, prd, resume, status, stop, doctor)
|
|
247
|
+
├── agents/ # 4 subagent prompts (executor, reviewer, researcher, debugger)
|
|
248
|
+
├── workflows/ # 6 core workflows (TDD, review, debug, research, deviation, execution-flow)
|
|
249
|
+
├── references/ # 8 reference docs
|
|
250
|
+
├── hooks/ # Session lifecycle (StatusLine + PostToolUse + SessionStart + Stop + AutoUpdate)
|
|
251
|
+
│ └── lib/ # Shared hook utilities (gsd-finder)
|
|
252
|
+
├── tests/ # 822 tests (unit + simulation + E2E)
|
|
232
253
|
├── cli.js # Install/uninstall CLI entry
|
|
233
254
|
├── install.js # Installation script
|
|
234
255
|
└── uninstall.js # Uninstall script
|
|
@@ -237,7 +258,7 @@ gsd-lite/
|
|
|
237
258
|
## Testing
|
|
238
259
|
|
|
239
260
|
```bash
|
|
240
|
-
npm test # Run all
|
|
261
|
+
npm test # Run all 822 tests
|
|
241
262
|
npm run test:coverage # Tests + coverage report (94%+ lines, 81%+ branches)
|
|
242
263
|
npm run lint # Biome lint
|
|
243
264
|
node --test tests/file.js # Run a single test file
|
package/agents/executor.md
CHANGED
|
@@ -55,6 +55,7 @@ tools: Read, Write, Edit, Bash, Grep, Glob
|
|
|
55
55
|
"decisions": ["[DECISION] use optimistic locking by version column"],
|
|
56
56
|
"blockers": [],
|
|
57
57
|
"contract_changed": true,
|
|
58
|
+
"confidence": "high",
|
|
58
59
|
"evidence": [
|
|
59
60
|
{"id": "ev:test:users-update", "scope": "task:2.3"},
|
|
60
61
|
{"id": "ev:typecheck:phase-2", "scope": "task:2.3"}
|
|
@@ -67,6 +68,13 @@ tools: Read, Write, Edit, Bash, Grep, Glob
|
|
|
67
68
|
- 改了共享类型定义 / 接口 → true
|
|
68
69
|
- 只改了内部实现逻辑、不影响外部调用方 → false
|
|
69
70
|
- 拿不准时 → true (安全优先)
|
|
71
|
+
|
|
72
|
+
`confidence` 判定指南 (用于审查级别自动调整):
|
|
73
|
+
- "high" — 测试全通过 + 改动明确 + 无意外复杂度
|
|
74
|
+
- "medium" — 测试通过但有不确定性 (边界条件、并发、外部依赖)
|
|
75
|
+
- "low" — 有已知风险/跳过的测试/不确定的副作用
|
|
76
|
+
- 拿不准时 → "medium"
|
|
77
|
+
- 编排器会根据 confidence 自动升/降审查级别
|
|
70
78
|
</result_contract>
|
|
71
79
|
|
|
72
80
|
<uncertainty_handling>
|
package/agents/researcher.md
CHANGED
|
@@ -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
|
|
package/agents/reviewer.md
CHANGED
|
@@ -58,12 +58,26 @@ L2 关键任务 → 单任务独立 review
|
|
|
58
58
|
- 拿不准时 → 升一级处理
|
|
59
59
|
</review_strategy>
|
|
60
60
|
|
|
61
|
+
<impact_analysis>
|
|
62
|
+
## 审查前影响分析 (多文件变更时)
|
|
63
|
+
|
|
64
|
+
当 `files_changed` 包含 3+ 文件,或涉及跨模块修改时:
|
|
65
|
+
1. 使用 `code-graph-mcp impact <主要变更的函数/类名>` 分析影响范围
|
|
66
|
+
2. 检查调用方是否都已被修改或兼容
|
|
67
|
+
3. 将未覆盖的影响范围标注为 Critical issue
|
|
68
|
+
|
|
69
|
+
这能发现 executor 遗漏的下游影响,是审查增值的关键步骤。
|
|
70
|
+
单文件内部修改可跳过此步骤。
|
|
71
|
+
如 `code-graph-mcp` 不可用,改用 Grep/Glob 手动追踪变更函数的调用方。
|
|
72
|
+
</impact_analysis>
|
|
73
|
+
|
|
61
74
|
<stage_1_spec_review>
|
|
62
75
|
检查代码是否符合任务规格:
|
|
63
76
|
- 所有需求都实现了吗?
|
|
64
77
|
- 有没有多余的实现 (YAGNI)?
|
|
65
78
|
- 接口/API 是否符合计划?
|
|
66
79
|
- 测试是否覆盖了需求中的每个场景?
|
|
80
|
+
- 影响分析发现的调用方是否都已适配?
|
|
67
81
|
结果: ✅ 通过 / ❌ 列出不符合项 (附具体代码位置)
|
|
68
82
|
</stage_1_spec_review>
|
|
69
83
|
|
package/commands/resume.md
CHANGED
|
@@ -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` 不同:
|
|
@@ -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.
|
|
6
|
-
// 4.
|
|
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(() => {});
|
package/hooks/hooks.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "GSD-Lite hooks: statusline
|
|
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/package.json
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
166
|
+
来源: `addEvidence()`, `_pruneEvidenceFromState()`, `pruneEvidence()`, `phaseComplete()` in `src/tools/state/`; `handleExecutorResult()`, `handleReviewerResult()` in `src/tools/orchestrator.js`
|
|
@@ -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
|
|
218
|
+
来源: `WORKFLOW_MODES` in `src/schema.js`, `resumeWorkflow()`, `evaluatePreflight()` in `src/tools/orchestrator.js`, `storeResearch()` in `src/tools/state/`
|
package/src/schema.js
CHANGED
|
@@ -586,6 +586,10 @@ export function validateExecutorResult(r) {
|
|
|
586
586
|
if (r.outcome === 'checkpointed' && typeof r.checkpoint_commit !== 'string') {
|
|
587
587
|
errors.push('checkpointed outcome requires checkpoint_commit');
|
|
588
588
|
}
|
|
589
|
+
// confidence is optional; when present must be one of the valid values
|
|
590
|
+
if ('confidence' in r && !['high', 'medium', 'low'].includes(r.confidence)) {
|
|
591
|
+
errors.push('confidence must be "high", "medium", or "low"');
|
|
592
|
+
}
|
|
589
593
|
return { valid: errors.length === 0, errors };
|
|
590
594
|
}
|
|
591
595
|
|
|
@@ -595,8 +599,8 @@ export function validateExecutorResult(r) {
|
|
|
595
599
|
export function validateReviewerResult(r) {
|
|
596
600
|
const errors = [];
|
|
597
601
|
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');
|
|
602
|
+
if (!(typeof r.scope_id === 'string' || typeof r.scope_id === 'number') || r.scope_id === '' || r.scope_id === 0) {
|
|
603
|
+
errors.push('missing or invalid scope_id');
|
|
600
604
|
}
|
|
601
605
|
if (!['L2', 'L1-batch', 'L1'].includes(r.review_level)) errors.push('invalid review_level (expected L2, L1-batch, or L1)');
|
|
602
606
|
if (typeof r.spec_passed !== 'boolean') errors.push('spec_passed must be boolean');
|
|
@@ -712,6 +716,8 @@ export function createInitialState({ project, phases }) {
|
|
|
712
716
|
if (!Array.isArray(phases)) {
|
|
713
717
|
return { error: true, message: 'phases must be an array' };
|
|
714
718
|
}
|
|
719
|
+
// Note: empty phases is allowed here for internal/test use;
|
|
720
|
+
// the public API guard is in init() which rejects phases.length === 0.
|
|
715
721
|
// Validate task names and uniqueness before creating state
|
|
716
722
|
const seenIds = new Set();
|
|
717
723
|
for (const [pi, p] of phases.entries()) {
|
|
@@ -741,6 +747,10 @@ export function createInitialState({ project, phases }) {
|
|
|
741
747
|
if (!['task', 'phase'].includes(dep.kind)) {
|
|
742
748
|
return { error: true, message: `Task ${taskId}: requires entry kind must be "task" or "phase" (got "${dep.kind}")` };
|
|
743
749
|
}
|
|
750
|
+
const validGates = ['checkpoint', 'accepted', 'phase_complete'];
|
|
751
|
+
if (dep.gate && !validGates.includes(dep.gate)) {
|
|
752
|
+
return { error: true, message: `Task ${taskId}: requires entry gate must be one of ${validGates.join(', ')} (got "${dep.gate}")` };
|
|
753
|
+
}
|
|
744
754
|
if (dep.kind === 'task' && !seenIds.has(String(dep.id))) {
|
|
745
755
|
return { error: true, message: `Task ${taskId}: requires references non-existent task "${dep.id}" (valid IDs: ${[...seenIds].join(', ')})` };
|
|
746
756
|
}
|