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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.mcp.json +0 -0
- package/README.md +33 -18
- package/agents/debugger.md +0 -0
- package/agents/executor.md +0 -0
- package/agents/researcher.md +24 -0
- package/agents/reviewer.md +0 -0
- package/commands/doctor.md +0 -0
- package/commands/prd.md +0 -0
- package/commands/resume.md +8 -0
- package/commands/start.md +0 -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 +0 -0
- package/hooks/gsd-context-monitor.cjs +0 -0
- package/hooks/gsd-session-init.cjs +104 -2
- package/hooks/gsd-session-stop.cjs +69 -0
- package/hooks/gsd-statusline.cjs +0 -0
- package/hooks/hooks.json +13 -1
- package/hooks/lib/gsd-finder.cjs +84 -0
- package/install.js +0 -0
- package/launcher.js +0 -0
- package/package.json +1 -1
- package/references/anti-rationalization-full.md +0 -0
- package/references/evidence-spec.md +3 -3
- package/references/execution-loop.md +0 -0
- package/references/git-worktrees.md +0 -0
- package/references/questioning.md +0 -0
- package/references/review-classification.md +1 -1
- package/references/state-diagram.md +1 -1
- package/references/testing-patterns.md +0 -0
- package/src/schema.js +8 -2
- package/src/server.js +32 -2
- package/src/tools/orchestrator/debugger.js +94 -0
- package/src/tools/orchestrator/executor.js +162 -0
- package/src/tools/orchestrator/helpers.js +447 -0
- package/src/tools/orchestrator/index.js +6 -0
- package/src/tools/orchestrator/researcher.js +27 -0
- package/src/tools/orchestrator/resume.js +457 -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} +279 -493
- package/src/tools/state/index.js +5 -0
- package/src/tools/state/logic.js +493 -0
- package/src/tools/verify.js +0 -0
- package/src/utils.js +0 -0
- package/uninstall.js +0 -0
- package/workflows/debugging.md +0 -0
- package/workflows/deviation-rules.md +0 -0
- package/workflows/execution-flow.md +0 -0
- package/workflows/research.md +0 -0
- package/workflows/review-cycle.md +0 -0
- package/workflows/tdd-cycle.md +0 -0
- 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.
|
|
16
|
+
"version": "0.5.12",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
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** —
|
|
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
|
|
@@ -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
|
-
###
|
|
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 (
|
|
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 | **
|
|
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 (
|
|
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
|
|
219
|
-
│ ├── server.js # MCP Server entry (
|
|
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
|
|
224
|
-
│ ├──
|
|
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/ #
|
|
227
|
-
├── agents/ # 4 subagent prompts
|
|
228
|
-
├── workflows/ #
|
|
229
|
-
├── references/ # 8 reference docs
|
|
230
|
-
├── hooks/ #
|
|
231
|
-
|
|
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
|
|
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
|
package/agents/debugger.md
CHANGED
|
File without changes
|
package/agents/executor.md
CHANGED
|
File without changes
|
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
|
File without changes
|
package/commands/doctor.md
CHANGED
|
File without changes
|
package/commands/prd.md
CHANGED
|
File without changes
|
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` 不同:
|
package/commands/start.md
CHANGED
|
File without changes
|
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
|
|
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.
|
|
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/gsd-statusline.cjs
CHANGED
|
File without changes
|
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/install.js
CHANGED
|
File without changes
|
package/launcher.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
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
|
|
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`
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -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/`
|
|
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
|
}
|