gsd-lite 0.6.7 → 0.6.9
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 +66 -19
- package/agents/executor.md +1 -0
- package/agents/researcher.md +25 -2
- package/commands/doctor.md +1 -1
- package/commands/resume.md +6 -6
- package/commands/stop.md +2 -0
- package/hooks/gsd-auto-update.cjs +50 -2
- package/hooks/gsd-session-init.cjs +25 -12
- package/hooks/lib/semver-sort.cjs +3 -3
- package/install.js +10 -2
- package/package.json +1 -1
- package/references/execution-loop.md +6 -2
- package/references/state-diagram.md +16 -6
- package/src/schema.js +1 -1
- package/src/server.js +1 -1
- package/src/tools/orchestrator/helpers.js +6 -6
- package/src/tools/state/constants.js +2 -1
- package/src/tools/state/crud.js +4 -3
- package/src/tools/state/logic.js +13 -0
- package/src/tools/verify.js +2 -2
- package/workflows/debugging.md +1 -1
- package/workflows/execution-flow.md +9 -1
- package/workflows/research.md +1 -1
|
@@ -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.6.
|
|
16
|
+
"version": "0.6.9",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Get Shit Done — AI orchestration for Claude Code
|
|
4
4
|
|
|
5
|
-
GSD-Lite is an AI orchestration tool for [Claude Code](https://docs.anthropic.com/en/docs/claude-code). It combines structured project management with built-in quality discipline: TDD enforcement, anti-rationalization guards, multi-level code review, and automatic failure recovery — all driven by a state machine that keeps multi-phase projects on track.
|
|
5
|
+
GSD-Lite is an AI orchestration tool for [Claude Code](https://docs.anthropic.com/en/docs/claude-code). It combines structured project management with built-in quality discipline: TDD enforcement, anti-rationalization guards, multi-level code review, and automatic failure recovery — all driven by a 12-state workflow machine that keeps multi-phase projects on track.
|
|
6
6
|
|
|
7
7
|
**Discuss thoroughly, execute automatically.** Have as many rounds of requirement discussion as needed. Once the plan is approved, GSD-Lite auto-executes: coding, self-review, independent review, verification, and phase advancement — with minimal human intervention.
|
|
8
8
|
|
|
@@ -10,7 +10,7 @@ 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
|
-
- **
|
|
13
|
+
- **12-state workflow machine** — `planning → executing_task → reviewing_task → reviewing_phase → completed` with precise transitions, persistent to `state.json`
|
|
14
14
|
- **Automatic task scheduling** — Gate-aware dependency resolution determines what runs next
|
|
15
15
|
- **Session resilience** — Stop anytime, resume exactly where you left off — crash protection via Stop hook auto-saves state markers
|
|
16
16
|
|
|
@@ -32,13 +32,19 @@ GSD-Lite is an AI orchestration tool for [Claude Code](https://docs.anthropic.co
|
|
|
32
32
|
- **Parallel task scheduling** — Independent tasks within the same phase are identified for concurrent dispatch
|
|
33
33
|
- **Auto PR suggestion** — Phase/project completion prompts PR creation with evidence summary
|
|
34
34
|
|
|
35
|
-
### Context Protection
|
|
35
|
+
### Context Protection & Monitoring
|
|
36
36
|
- **Subagent isolation** — Each task runs in its own agent context, preventing cross-contamination
|
|
37
|
-
- **
|
|
37
|
+
- **Real-time context health monitoring** — StatusLine tracks context usage and project phase; composite StatusLine support coexists with other plugins
|
|
38
38
|
- **Session lifecycle hooks** — Stop hook writes crash marker; SessionStart injects project status into CLAUDE.md; resume detects non-graceful exits
|
|
39
39
|
- **Evidence-based verification** — Every claim backed by command output, not assertions
|
|
40
40
|
- **Research with TTL** — Research artifacts include volatility ratings and expiration dates
|
|
41
41
|
|
|
42
|
+
### Auto-Update & Version Management
|
|
43
|
+
- **Automatic update checks** — Checks GitHub Releases every 24 hours with rate-limit backoff
|
|
44
|
+
- **Version drift detection** — Server startup compares running version against disk and plugin registry, warns on mismatch
|
|
45
|
+
- **Smart cache management** — Keeps latest 3 cached versions, auto-prunes old entries
|
|
46
|
+
- **Idempotent installer** — Reinstall anytime without uninstalling; legacy files auto-cleaned
|
|
47
|
+
|
|
42
48
|
## Architecture
|
|
43
49
|
|
|
44
50
|
```
|
|
@@ -54,8 +60,8 @@ User → discuss + research (confirm requirements) → approve plan → auto-exe
|
|
|
54
60
|
|---------|---------|
|
|
55
61
|
| `/gsd:start` | Interactive start — discuss requirements, research, plan, then auto-execute |
|
|
56
62
|
| `/gsd:prd <input>` | Start from a requirements doc or description text |
|
|
57
|
-
| `/gsd:resume` | Resume execution from saved state |
|
|
58
|
-
| `/gsd:status` | View project progress dashboard |
|
|
63
|
+
| `/gsd:resume` | Resume execution from saved state with workspace validation |
|
|
64
|
+
| `/gsd:status` | View project progress dashboard (derived from canonical state fields) |
|
|
59
65
|
| `/gsd:stop` | Save state and pause execution |
|
|
60
66
|
| `/gsd:doctor` | Diagnostic checks on GSD-Lite installation and project health |
|
|
61
67
|
|
|
@@ -68,6 +74,17 @@ User → discuss + research (confirm requirements) → approve plan → auto-exe
|
|
|
68
74
|
| **researcher** | Ecosystem research (Context7 → official docs → web) | Confidence scoring + TTL |
|
|
69
75
|
| **debugger** | 4-phase systematic root cause analysis | Root Cause Iron Law |
|
|
70
76
|
|
|
77
|
+
### 6 Workflows
|
|
78
|
+
|
|
79
|
+
| Workflow | Purpose |
|
|
80
|
+
|----------|---------|
|
|
81
|
+
| `tdd-cycle` | RED-GREEN-REFACTOR TDD cycle enforcement |
|
|
82
|
+
| `review-cycle` | Two-level review gates and accept/rework decisions |
|
|
83
|
+
| `debugging` | 4-phase root cause analysis process |
|
|
84
|
+
| `research` | Research with confidence scoring and TTL expiration |
|
|
85
|
+
| `deviation-rules` | Anti-rationalization guards and red-flag checklists |
|
|
86
|
+
| `execution-flow` | Complete task execution cycle from dispatch to checkpoint |
|
|
87
|
+
|
|
71
88
|
### MCP Server (11 Tools)
|
|
72
89
|
|
|
73
90
|
| Tool | Purpose |
|
|
@@ -84,6 +101,19 @@ User → discuss + research (confirm requirements) → approve plan → auto-exe
|
|
|
84
101
|
| `orchestrator-handle-researcher-result` | Store research artifacts and decisions |
|
|
85
102
|
| `orchestrator-handle-debugger-result` | Process root cause analysis, re-dispatch executor |
|
|
86
103
|
|
|
104
|
+
### 8 References
|
|
105
|
+
|
|
106
|
+
| Reference | Content |
|
|
107
|
+
|-----------|---------|
|
|
108
|
+
| `execution-loop` | 9-step execution loop specification (single source of truth) |
|
|
109
|
+
| `review-classification` | Review level classification decision tree (L0/L1/L2) |
|
|
110
|
+
| `evidence-spec` | Evidence validation and citation rules |
|
|
111
|
+
| `state-diagram` | 12-state lifecycle workflow machine diagram |
|
|
112
|
+
| `testing-patterns` | Test structure and patterns |
|
|
113
|
+
| `anti-rationalization-full` | Full red-flag checklist for agents |
|
|
114
|
+
| `git-worktrees` | Git worktree isolation strategy |
|
|
115
|
+
| `questioning` | Requirements clarification patterns |
|
|
116
|
+
|
|
87
117
|
## Installation
|
|
88
118
|
|
|
89
119
|
### Method 1: Claude Code Plugin (Recommended)
|
|
@@ -96,7 +126,7 @@ User → discuss + research (confirm requirements) → approve plan → auto-exe
|
|
|
96
126
|
/plugin install gsd
|
|
97
127
|
```
|
|
98
128
|
|
|
99
|
-
Automatically registers all commands, agents, workflows, MCP server, and
|
|
129
|
+
Automatically registers all commands, agents, workflows, MCP server, hooks, and auto-update. Run these commands inside a Claude Code session.
|
|
100
130
|
|
|
101
131
|
### Method 2: npx
|
|
102
132
|
|
|
@@ -113,12 +143,14 @@ cd gsd-lite && npm install && node cli.js install
|
|
|
113
143
|
|
|
114
144
|
Methods 2 & 3 write components to `~/.claude/` and register the MCP server in `settings.json`.
|
|
115
145
|
|
|
146
|
+
The installer copies commands, agents, workflows, references, and hooks to `~/.claude/`, and sets up the MCP server runtime in `~/.claude/gsd/`.
|
|
147
|
+
|
|
116
148
|
Uninstall: `node cli.js uninstall` or `npx gsd-lite uninstall`
|
|
117
149
|
|
|
118
150
|
## Upgrade
|
|
119
151
|
|
|
120
152
|
```bash
|
|
121
|
-
# Plugin
|
|
153
|
+
# Plugin (auto-update checks GitHub Releases every 24h)
|
|
122
154
|
/plugin update gsd
|
|
123
155
|
|
|
124
156
|
# npx
|
|
@@ -130,6 +162,7 @@ git pull && npm install && node cli.js install
|
|
|
130
162
|
|
|
131
163
|
- Installer is idempotent — no need to uninstall first
|
|
132
164
|
- Upgrades from older versions auto-clean legacy files
|
|
165
|
+
- Smart cache management keeps latest 3 versions, prunes old entries
|
|
133
166
|
- Restart Claude Code after updating to load new MCP server / hooks
|
|
134
167
|
|
|
135
168
|
## Quick Start
|
|
@@ -204,8 +237,10 @@ executor retries → with debugger guidance injected
|
|
|
204
237
|
All state lives in `.gsd/state.json` — a single source of truth with:
|
|
205
238
|
- Canonical fields (whitelist-controlled, schema-validated)
|
|
206
239
|
- Lifecycle state machine (pending → running → checkpointed → accepted)
|
|
240
|
+
- Optimistic concurrency control (`_version` field with `VERSION_CONFLICT` detection)
|
|
207
241
|
- Evidence references (command outputs, test results)
|
|
208
242
|
- Research artifacts and decision index
|
|
243
|
+
- Incremental validation (simple field updates use fast path; phases use full validation)
|
|
209
244
|
|
|
210
245
|
## Comparison with GSD
|
|
211
246
|
|
|
@@ -213,20 +248,22 @@ All state lives in `.gsd/state.json` — a single source of truth with:
|
|
|
213
248
|
|-----------|-----|----------|
|
|
214
249
|
| Commands | 32 | **6** |
|
|
215
250
|
| Agents | 12 | **4** |
|
|
216
|
-
| Source files | 100+ | **~
|
|
251
|
+
| Source files | 100+ | **~15** |
|
|
217
252
|
| Installer | 2465 lines | **~290 lines** |
|
|
218
253
|
| User interactions | 6+ confirmations | **Typically 2** |
|
|
219
254
|
| TDD / Anti-rationalization | No | **Yes** |
|
|
220
255
|
| State machine recovery | Partial | **Full (12 modes)** |
|
|
221
256
|
| Evidence-based verification | No | **Yes** |
|
|
257
|
+
| Auto-update | No | **Yes** |
|
|
258
|
+
| Context health monitoring | No | **Yes** |
|
|
222
259
|
|
|
223
260
|
## Project Structure
|
|
224
261
|
|
|
225
262
|
```
|
|
226
263
|
gsd-lite/
|
|
227
|
-
├── src/ # MCP Server + tools
|
|
228
|
-
│ ├── server.js # MCP Server entry (11 tools)
|
|
229
|
-
│ ├── schema.js # State schema + lifecycle validation
|
|
264
|
+
├── src/ # MCP Server + tools (15 source files)
|
|
265
|
+
│ ├── server.js # MCP Server entry (11 tools + version drift detection)
|
|
266
|
+
│ ├── schema.js # State schema + lifecycle validation + incremental validation
|
|
230
267
|
│ ├── utils.js # Shared utilities (atomic writes, git, file lock)
|
|
231
268
|
│ └── tools/
|
|
232
269
|
│ ├── state/ # State management (modular)
|
|
@@ -236,7 +273,7 @@ gsd-lite/
|
|
|
236
273
|
│ │ └── index.js # Re-exports
|
|
237
274
|
│ ├── orchestrator/ # Orchestration logic (modular)
|
|
238
275
|
│ │ ├── helpers.js # Shared constants, preflight, dispatch
|
|
239
|
-
│ │ ├── resume.js # Workflow resume state machine
|
|
276
|
+
│ │ ├── resume.js # Workflow resume state machine (12 modes)
|
|
240
277
|
│ │ ├── executor.js # Executor result handler
|
|
241
278
|
│ │ ├── reviewer.js # Reviewer result handler
|
|
242
279
|
│ │ ├── debugger.js # Debugger result handler
|
|
@@ -246,19 +283,24 @@ gsd-lite/
|
|
|
246
283
|
├── commands/ # 6 slash commands (start, prd, resume, status, stop, doctor)
|
|
247
284
|
├── agents/ # 4 subagent prompts (executor, reviewer, researcher, debugger)
|
|
248
285
|
├── workflows/ # 6 core workflows (TDD, review, debug, research, deviation, execution-flow)
|
|
249
|
-
├── references/ # 8 reference docs
|
|
250
|
-
├── hooks/ # Session lifecycle
|
|
251
|
-
│
|
|
252
|
-
├──
|
|
286
|
+
├── references/ # 8 reference docs (execution-loop, state-diagram, evidence-spec, etc.)
|
|
287
|
+
├── hooks/ # Session lifecycle hooks
|
|
288
|
+
│ ├── gsd-auto-update.cjs # Auto-update from GitHub Releases (24h check interval)
|
|
289
|
+
│ ├── gsd-context-monitor.cjs # Real-time context health monitoring
|
|
290
|
+
│ ├── gsd-session-init.cjs # Session initialization + CLAUDE.md status injection
|
|
291
|
+
│ ├── gsd-session-stop.cjs # Graceful shutdown with crash markers
|
|
292
|
+
│ ├── gsd-statusline.cjs # StatusLine display (composite-aware)
|
|
293
|
+
│ └── lib/ # Shared hook utilities (gsd-finder, composite statusline, semver)
|
|
294
|
+
├── tests/ # 909 tests (unit + simulation + E2E integration)
|
|
253
295
|
├── cli.js # Install/uninstall CLI entry
|
|
254
|
-
├── install.js # Installation script
|
|
296
|
+
├── install.js # Installation script (plugin-aware, idempotent)
|
|
255
297
|
└── uninstall.js # Uninstall script
|
|
256
298
|
```
|
|
257
299
|
|
|
258
300
|
## Testing
|
|
259
301
|
|
|
260
302
|
```bash
|
|
261
|
-
npm test # Run all
|
|
303
|
+
npm test # Run all 909 tests
|
|
262
304
|
npm run test:coverage # Tests + coverage report (94%+ lines, 83%+ branches)
|
|
263
305
|
npm run lint # Biome lint
|
|
264
306
|
node --test tests/file.js # Run a single test file
|
|
@@ -270,6 +312,11 @@ node --test tests/file.js # Run a single test file
|
|
|
270
312
|
- [Engineering Tasks](docs/gsd-lite-engineering-tasks.md) — 38 implementation tasks (5 phases, all complete)
|
|
271
313
|
- [Calibration Notes](docs/calibration-notes.md) — Context threshold and TTL calibration
|
|
272
314
|
|
|
315
|
+
## Requirements
|
|
316
|
+
|
|
317
|
+
- Node.js >= 20.0.0
|
|
318
|
+
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code)
|
|
319
|
+
|
|
273
320
|
## License
|
|
274
321
|
|
|
275
322
|
MIT
|
package/agents/executor.md
CHANGED
|
@@ -56,6 +56,7 @@ tools: Read, Write, Edit, Bash, Grep, Glob
|
|
|
56
56
|
"blockers": [],
|
|
57
57
|
"contract_changed": true,
|
|
58
58
|
"confidence": "high",
|
|
59
|
+
"error_fingerprint": "optional string — short fingerprint for 3-strike deduplication (file+line or msg[:50])",
|
|
59
60
|
"evidence": [
|
|
60
61
|
{"id": "ev:test:users-update", "scope": "task:2.3"},
|
|
61
62
|
{"id": "ev:typecheck:phase-2", "scope": "task:2.3"}
|
package/agents/researcher.md
CHANGED
|
@@ -68,7 +68,30 @@ tools: Read, Write, Bash, WebSearch, WebFetch, mcp__plugin_context7_context7__*
|
|
|
68
68
|
## 遇到不确定性时
|
|
69
69
|
子代理不能直接与用户交互。遇到不确定性时:
|
|
70
70
|
1. 来源冲突 → 报告双方立场及置信度,让编排器决定。在 result 中标注 "[DECISION] 选择了X因为Y"
|
|
71
|
-
2. 所有来源不可用 (Context7 + WebSearch + 官方文档均失败) →
|
|
72
|
-
|
|
71
|
+
2. 所有来源不可用 (Context7 + WebSearch + 官方文档均失败) → 仍然返回有效的 result contract JSON (编排器需要通过 `validateResearcherResult` 校验),在 decision 摘要中标注阻塞原因:
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"result": {
|
|
75
|
+
"decision_ids": ["decision:blocked-no-sources"],
|
|
76
|
+
"volatility": "high",
|
|
77
|
+
"expires_at": "<24h后的ISO时间>",
|
|
78
|
+
"sources": []
|
|
79
|
+
},
|
|
80
|
+
"decision_index": {
|
|
81
|
+
"decision:blocked-no-sources": {
|
|
82
|
+
"summary": "[BLOCKED] 研究来源不可用,请提供替代信息或缩小范围",
|
|
83
|
+
"source": "none",
|
|
84
|
+
"expires_at": "<24h后的ISO时间>"
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
"artifacts": {
|
|
88
|
+
"STACK.md": "# 研究受阻\n来源不可用,无法完成研究。",
|
|
89
|
+
"ARCHITECTURE.md": "# 研究受阻\n来源不可用。",
|
|
90
|
+
"PITFALLS.md": "# 研究受阻\n来源不可用。",
|
|
91
|
+
"SUMMARY.md": "# 研究受阻\n所有来源 (Context7/WebSearch/官方文档) 均不可用。需要用户提供替代信息或缩小范围。"
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
3. 研究范围过广无法收敛 → 同上模式,decision 摘要改为 "[BLOCKED] 研究范围过广,请指定重点领域"
|
|
73
96
|
4. 发现结论与已有 decisions 矛盾 → 在 result 中标注冲突,让编排器决定是否更新 decision
|
|
74
97
|
</uncertainty_handling>
|
package/commands/doctor.md
CHANGED
|
@@ -44,7 +44,7 @@ Also verify the hook files exist on disk:
|
|
|
44
44
|
|
|
45
45
|
## STEP 4: Lock File Check
|
|
46
46
|
|
|
47
|
-
Check if `.gsd
|
|
47
|
+
Check if `.gsd/state.lock` exists:
|
|
48
48
|
- If not exists: record PASS "No stale lock"
|
|
49
49
|
- If exists: check file age
|
|
50
50
|
- Older than 5 minutes: record WARN "Stale lock file detected (age: {age}). May indicate a crashed process. Consider removing it."
|
package/commands/resume.md
CHANGED
|
@@ -51,16 +51,16 @@ description: Resume project execution from saved state with workspace validation
|
|
|
51
51
|
- 如果当前或任何未完成 phase 的 `phase_handoff.direction_ok === false`
|
|
52
52
|
- → 覆写 `workflow_mode = awaiting_user`
|
|
53
53
|
|
|
54
|
-
4.
|
|
54
|
+
4. **Dirty-phase 回滚检测:**
|
|
55
|
+
- 检查 `current_phase` 之前的 phase (`p.id < current_phase`) 中是否有 `needs_revalidation` 状态的 task
|
|
56
|
+
- 如有 → 回滚 `current_phase` 到最早的 dirty phase
|
|
57
|
+
- → 覆写 `workflow_mode = executing_task`
|
|
58
|
+
|
|
59
|
+
5. **研究过期校验:**
|
|
55
60
|
- 如果 `research.expires_at` 已过期 (早于当前时间)
|
|
56
61
|
- 或 research.decision_index 中有条目的 expires_at 已过期
|
|
57
62
|
- → 覆写 `workflow_mode = research_refresh_needed`
|
|
58
63
|
|
|
59
|
-
5. **Dirty-phase 回滚检测:**
|
|
60
|
-
- 检查已完成 phase 中是否有 `needs_revalidation` 状态的 task
|
|
61
|
-
- 如有 → 回滚 `current_phase` 到最早的 dirty phase
|
|
62
|
-
- → 覆写 `workflow_mode = executing_task`
|
|
63
|
-
|
|
64
64
|
6. **全部通过:**
|
|
65
65
|
- 保持原 `workflow_mode` 不变
|
|
66
66
|
|
package/commands/stop.md
CHANGED
|
@@ -324,12 +324,37 @@ function validateExtractedPackage(extractDir) {
|
|
|
324
324
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
325
325
|
if (pkg.name !== 'gsd-lite') return false;
|
|
326
326
|
if (!pkg.version || !/^\d+\.\d+\.\d+/.test(pkg.version)) return false;
|
|
327
|
+
// Verify install.js exists and is a regular file (lstat rejects symlinks)
|
|
328
|
+
const installPath = path.join(extractDir, 'install.js');
|
|
329
|
+
const lstat = fs.lstatSync(installPath);
|
|
330
|
+
if (!lstat.isFile()) return false;
|
|
327
331
|
return true;
|
|
328
332
|
} catch {
|
|
329
333
|
return false;
|
|
330
334
|
}
|
|
331
335
|
}
|
|
332
336
|
|
|
337
|
+
// ── Tarball URL Validation ─────────────────────────────────
|
|
338
|
+
const ALLOWED_TARBALL_HOSTS = [
|
|
339
|
+
'github.com',
|
|
340
|
+
'api.github.com',
|
|
341
|
+
'codeload.github.com',
|
|
342
|
+
'objects.githubusercontent.com',
|
|
343
|
+
];
|
|
344
|
+
|
|
345
|
+
function validateTarballUrl(url) {
|
|
346
|
+
if (!url) return false;
|
|
347
|
+
try {
|
|
348
|
+
const parsed = new URL(url);
|
|
349
|
+
if (parsed.protocol !== 'https:') return false;
|
|
350
|
+
return ALLOWED_TARBALL_HOSTS.some(
|
|
351
|
+
allowed => parsed.hostname === allowed || parsed.hostname.endsWith('.' + allowed),
|
|
352
|
+
);
|
|
353
|
+
} catch {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
333
358
|
// ── Download & Install ─────────────────────────────────────
|
|
334
359
|
async function downloadAndInstall(tarballUrl, verbose = false, token = null) {
|
|
335
360
|
const tmpDir = path.join(os.tmpdir(), `gsd-update-${Date.now()}`);
|
|
@@ -340,6 +365,9 @@ async function downloadAndInstall(tarballUrl, verbose = false, token = null) {
|
|
|
340
365
|
|
|
341
366
|
// Download tarball via fetch (no shell interpolation)
|
|
342
367
|
if (verbose) console.log(' Downloading tarball...');
|
|
368
|
+
if (!validateTarballUrl(tarballUrl)) {
|
|
369
|
+
throw new Error(`Tarball URL failed host validation: ${(() => { try { return new URL(tarballUrl).hostname; } catch { return tarballUrl; } })()}`);
|
|
370
|
+
}
|
|
343
371
|
const headers = { Accept: 'application/vnd.github+json', 'User-Agent': 'gsd-lite-auto-update/1.0' };
|
|
344
372
|
if (token) headers.Authorization = `Bearer ${token}`;
|
|
345
373
|
|
|
@@ -347,7 +375,26 @@ async function downloadAndInstall(tarballUrl, verbose = false, token = null) {
|
|
|
347
375
|
const dlTimeout = setTimeout(() => controller.abort(), 30000);
|
|
348
376
|
let tarData;
|
|
349
377
|
try {
|
|
350
|
-
|
|
378
|
+
let res = await fetch(tarballUrl, { signal: controller.signal, headers, redirect: 'manual' });
|
|
379
|
+
// Handle redirect manually to prevent Authorization header leakage
|
|
380
|
+
if (res.status === 301 || res.status === 302) {
|
|
381
|
+
const location = res.headers.get('location');
|
|
382
|
+
if (!location || !validateTarballUrl(location)) {
|
|
383
|
+
throw new Error(`Redirect URL failed host validation: ${location || '(empty)'}`);
|
|
384
|
+
}
|
|
385
|
+
// Follow redirect WITHOUT Authorization header (prevent token leakage to CDN)
|
|
386
|
+
// Use redirect: 'manual' to validate any further redirects in the chain
|
|
387
|
+
const redirectHeaders = { Accept: 'application/vnd.github+json', 'User-Agent': 'gsd-lite-auto-update/1.0' };
|
|
388
|
+
res = await fetch(location, { signal: controller.signal, headers: redirectHeaders, redirect: 'manual' });
|
|
389
|
+
// Handle one more potential redirect from CDN (e.g., 303/307/308)
|
|
390
|
+
if (res.status >= 300 && res.status < 400) {
|
|
391
|
+
const loc2 = res.headers.get('location');
|
|
392
|
+
if (!loc2 || !validateTarballUrl(loc2)) {
|
|
393
|
+
throw new Error(`Secondary redirect URL failed host validation: ${loc2 || '(empty)'}`);
|
|
394
|
+
}
|
|
395
|
+
res = await fetch(loc2, { signal: controller.signal, headers: redirectHeaders, redirect: 'error' });
|
|
396
|
+
}
|
|
397
|
+
}
|
|
351
398
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
352
399
|
tarData = Buffer.from(await res.arrayBuffer());
|
|
353
400
|
} finally {
|
|
@@ -452,7 +499,7 @@ function pruneOldCacheVersions(cacheBase, keepCount = 3, verbose = false) {
|
|
|
452
499
|
try {
|
|
453
500
|
if (!fs.existsSync(cacheBase)) return;
|
|
454
501
|
const entries = fs.readdirSync(cacheBase, { withFileTypes: true })
|
|
455
|
-
.filter(e => e.isDirectory())
|
|
502
|
+
.filter(e => e.isDirectory() && /^\d+\.\d+\.\d+$/.test(e.name))
|
|
456
503
|
.map(e => e.name);
|
|
457
504
|
if (entries.length <= keepCount) return;
|
|
458
505
|
|
|
@@ -581,6 +628,7 @@ module.exports = {
|
|
|
581
628
|
shouldCheck,
|
|
582
629
|
shouldSkipUpdateCheck,
|
|
583
630
|
validateExtractedPackage,
|
|
631
|
+
validateTarballUrl,
|
|
584
632
|
};
|
|
585
633
|
|
|
586
634
|
// ── CLI Entry Point (for background auto-install) ─────────
|
|
@@ -53,11 +53,20 @@ setTimeout(() => process.exit(0), 4000).unref();
|
|
|
53
53
|
const stableStatuslinePath = path.join(claudeDir, 'hooks', 'gsd-statusline.cjs');
|
|
54
54
|
if (fs.existsSync(stableStatuslinePath)) {
|
|
55
55
|
let settings = {};
|
|
56
|
+
let settingsParseError = false;
|
|
56
57
|
try {
|
|
57
58
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
58
|
-
} catch {
|
|
59
|
+
} catch (e) {
|
|
60
|
+
if (e.code === 'ENOENT') {
|
|
61
|
+
settings = {}; // File doesn't exist — create fresh
|
|
62
|
+
} else {
|
|
63
|
+
// Parse error or other — skip write to avoid overwriting corrupted file
|
|
64
|
+
if (process.env.GSD_DEBUG) console.error('[gsd-session-init] settings.json read error:', e.message);
|
|
65
|
+
settingsParseError = true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
59
68
|
|
|
60
|
-
if (settings) {
|
|
69
|
+
if (!settingsParseError && settings) {
|
|
61
70
|
const current = settings.statusLine?.command || '';
|
|
62
71
|
|
|
63
72
|
if (current.includes('gsd-statusline')) {
|
|
@@ -120,12 +129,13 @@ setTimeout(() => process.exit(0), 4000).unref();
|
|
|
120
129
|
const notifPath = path.join(claudeDir, 'gsd', 'runtime', 'update-notification.json');
|
|
121
130
|
if (fs.existsSync(notifPath)) {
|
|
122
131
|
const notif = JSON.parse(fs.readFileSync(notifPath, 'utf8'));
|
|
132
|
+
const safeSemver = (s) => /^\d+\.\d+\.\d+/.test(String(s || '')) ? String(s) : '?.?.?';
|
|
123
133
|
if (notif.kind === 'updated') {
|
|
124
|
-
console.log(`✅ GSD-Lite auto-updated: v${notif.from} → v${notif.to}`);
|
|
134
|
+
console.log(`✅ GSD-Lite auto-updated: v${safeSemver(notif.from)} → v${safeSemver(notif.to)}`);
|
|
125
135
|
} else if (notif.kind === 'available' && notif.action === 'plugin_update') {
|
|
126
|
-
console.log(`📦 GSD-Lite update available: v${notif.from} → v${notif.to}. Run /plugin update gsd`);
|
|
136
|
+
console.log(`📦 GSD-Lite update available: v${safeSemver(notif.from)} → v${safeSemver(notif.to)}. Run /plugin update gsd`);
|
|
127
137
|
} else if (notif.kind === 'available') {
|
|
128
|
-
console.log(`📦 GSD-Lite update available: v${notif.from} → v${notif.to}. Run gsd update`);
|
|
138
|
+
console.log(`📦 GSD-Lite update available: v${safeSemver(notif.from)} → v${safeSemver(notif.to)}. Run gsd update`);
|
|
129
139
|
}
|
|
130
140
|
fs.unlinkSync(notifPath);
|
|
131
141
|
}
|
|
@@ -163,11 +173,14 @@ setTimeout(() => process.exit(0), 4000).unref();
|
|
|
163
173
|
}
|
|
164
174
|
} catch { /* skip */ }
|
|
165
175
|
|
|
176
|
+
// Sanitize user-controlled strings to prevent HTML/markdown injection
|
|
177
|
+
const safeName = (s) => String(s || '').replace(/<!--|-->/g, '').slice(0, 200);
|
|
178
|
+
|
|
166
179
|
// Stdout: only output session-end warning (crash recovery), skip routine progress
|
|
167
180
|
// Routine progress is handled by CLAUDE.md injection below — avoids noise
|
|
168
181
|
const shortHead = progress.gitHead ? progress.gitHead.substring(0, 7) : 'n/a';
|
|
169
182
|
if (sessionEndInfo) {
|
|
170
|
-
console.log(`⚠️ GSD: Previous session ended unexpectedly at ${sessionEndInfo.ended_at} (was: ${sessionEndInfo.workflow_mode_was}). Run /gsd:resume to recover.`);
|
|
183
|
+
console.log(`⚠️ GSD: Previous session ended unexpectedly at ${sessionEndInfo.ended_at} (was: ${safeName(sessionEndInfo.workflow_mode_was)}). Run /gsd:resume to recover.`);
|
|
171
184
|
}
|
|
172
185
|
|
|
173
186
|
// Write status block to CLAUDE.md
|
|
@@ -178,13 +191,13 @@ setTimeout(() => process.exit(0), 4000).unref();
|
|
|
178
191
|
|
|
179
192
|
const statusBlock = [
|
|
180
193
|
BEGIN_MARKER,
|
|
181
|
-
`### GSD Project: ${progress.project}`,
|
|
182
|
-
`- Phase: ${progress.currentPhase || '?'}/${progress.totalPhases} (${progress.phaseName})`,
|
|
183
|
-
`- Task: ${progress.currentTask || 'none'}${progress.taskName ? ` (${progress.taskName})` : ''}`,
|
|
184
|
-
`- Mode: ${progress.workflowMode}`,
|
|
194
|
+
`### GSD Project: ${safeName(progress.project)}`,
|
|
195
|
+
`- Phase: ${progress.currentPhase || '?'}/${progress.totalPhases} (${safeName(progress.phaseName)})`,
|
|
196
|
+
`- Task: ${progress.currentTask || 'none'}${progress.taskName ? ` (${safeName(progress.taskName)})` : ''}`,
|
|
197
|
+
`- Mode: ${safeName(progress.workflowMode)}`,
|
|
185
198
|
`- Progress: ${progress.acceptedTasks}/${progress.totalTasks} tasks done`,
|
|
186
|
-
`- Last checkpoint: ${shortHead}`,
|
|
187
|
-
sessionEndInfo ? `- ⚠️ Previous session ended unexpectedly (${sessionEndInfo.ended_at})` : null,
|
|
199
|
+
`- Last checkpoint: ${safeName(shortHead)}`,
|
|
200
|
+
sessionEndInfo ? `- ⚠️ Previous session ended unexpectedly (${safeName(sessionEndInfo.ended_at)})` : null,
|
|
188
201
|
END_MARKER,
|
|
189
202
|
].filter(Boolean).join('\n');
|
|
190
203
|
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
* @returns {number}
|
|
10
10
|
*/
|
|
11
11
|
function semverSortComparator(a, b) {
|
|
12
|
-
const pa = a.split('.').map(
|
|
13
|
-
const pb = b.split('.').map(
|
|
12
|
+
const pa = a.split('.').map(s => parseInt(s, 10) || 0);
|
|
13
|
+
const pb = b.split('.').map(s => parseInt(s, 10) || 0);
|
|
14
14
|
for (let i = 0; i < 3; i++) {
|
|
15
|
-
if (
|
|
15
|
+
if (pa[i] !== pb[i]) return pa[i] - pb[i];
|
|
16
16
|
}
|
|
17
17
|
return 0;
|
|
18
18
|
}
|
package/install.js
CHANGED
|
@@ -183,6 +183,11 @@ export function main() {
|
|
|
183
183
|
// 6. Stable runtime for MCP server
|
|
184
184
|
copyDir(join(__dirname, 'src'), join(RUNTIME_DIR, 'src'), 'runtime/src → ~/.claude/gsd/src/');
|
|
185
185
|
copyFile(join(__dirname, 'package.json'), join(RUNTIME_DIR, 'package.json'), 'runtime/package.json → ~/.claude/gsd/package.json');
|
|
186
|
+
// Copy lock file so `npm ci` works when node_modules are not present (npx scenario)
|
|
187
|
+
const lockFile = join(__dirname, 'package-lock.json');
|
|
188
|
+
if (existsSync(lockFile)) {
|
|
189
|
+
copyFile(lockFile, join(RUNTIME_DIR, 'package-lock.json'), 'runtime/package-lock.json → ~/.claude/gsd/package-lock.json');
|
|
190
|
+
}
|
|
186
191
|
|
|
187
192
|
// 7. Runtime dependencies — copy local node_modules or install fresh (npx hoists deps)
|
|
188
193
|
const localNM = join(__dirname, 'node_modules');
|
|
@@ -190,8 +195,11 @@ export function main() {
|
|
|
190
195
|
copyDir(localNM, join(RUNTIME_DIR, 'node_modules'), 'runtime/node_modules (copied)');
|
|
191
196
|
} else if (!DRY_RUN) {
|
|
192
197
|
log(' ⧗ Installing runtime dependencies...');
|
|
198
|
+
const lockFile = join(RUNTIME_DIR, 'package-lock.json');
|
|
199
|
+
const hasLockFile = existsSync(lockFile);
|
|
200
|
+
const installCmd = hasLockFile ? 'npm ci --omit=dev' : 'npm install --omit=dev --no-fund --no-audit';
|
|
193
201
|
try {
|
|
194
|
-
execSync(
|
|
202
|
+
execSync(installCmd, { cwd: RUNTIME_DIR, stdio: 'pipe' });
|
|
195
203
|
log(' ✓ runtime dependencies installed');
|
|
196
204
|
} catch (err) {
|
|
197
205
|
log(` ✗ Failed to install runtime dependencies: ${err.message}`);
|
|
@@ -264,7 +272,7 @@ export function main() {
|
|
|
264
272
|
if (existsSync(cacheBase)) {
|
|
265
273
|
try {
|
|
266
274
|
const entries = readdirSync(cacheBase, { withFileTypes: true })
|
|
267
|
-
.filter(e => e.isDirectory()).map(e => e.name);
|
|
275
|
+
.filter(e => e.isDirectory() && /^\d+\.\d+\.\d+$/.test(e.name)).map(e => e.name);
|
|
268
276
|
if (entries.length > 3) {
|
|
269
277
|
const sorted = entries.slice().sort(semverSortComparator);
|
|
270
278
|
// Detect versions with active processes to avoid disrupting running sessions
|
package/package.json
CHANGED
|
@@ -33,8 +33,10 @@ executor 上下文传递协议 (orchestrator → executor):
|
|
|
33
33
|
├── research_decisions: 从 research_basis 引用的 decision 摘要
|
|
34
34
|
├── predecessor_outputs: 前置依赖 task 的 files_changed + checkpoint_commit
|
|
35
35
|
├── project_conventions: CLAUDE.md 路径 (executor 自行读取)
|
|
36
|
-
├── workflows: 需加载的工作流文件路径 (如 tdd-cycle.md)
|
|
37
|
-
|
|
36
|
+
├── workflows: 需加载的工作流文件路径 (如 tdd-cycle.md, deviation-rules.md; retry 时追加 debugging.md; 有 research_basis 时追加 research.md)
|
|
37
|
+
├── constraints: retry_count / level / review_required
|
|
38
|
+
├── debugger_guidance: debugger 分析结果 (root_cause / fix_direction / fix_attempts / evidence),仅在 debug_context 存在时提供,否则 null
|
|
39
|
+
└── rework_feedback: reviewer 返工反馈 (issue 描述数组),仅在 last_review_feedback 存在时提供,否则 null
|
|
38
40
|
```
|
|
39
41
|
|
|
40
42
|
派发 `executor` 子代理执行单个 task。
|
|
@@ -146,6 +148,8 @@ remaining <= 25%:
|
|
|
146
148
|
4. 立即停止
|
|
147
149
|
```
|
|
148
150
|
|
|
151
|
+
> **Note:** 上述 35%/25% 阈值为编排器主动发起上下文保存的建议阈值。Resume 时的恢复阻断阈值为 `CONTEXT_RESUME_THRESHOLD = 40`(服务端强制校验),低于 40% 时 resume 会拒绝恢复并要求 /clear。
|
|
152
|
+
|
|
149
153
|
---
|
|
150
154
|
|
|
151
155
|
## 依赖门槛语义 (Gate-aware dependencies)
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
| 当前状态 | 允许的目标状态 |
|
|
8
8
|
|----------|---------------|
|
|
9
9
|
| `pending` | `running`, `blocked` |
|
|
10
|
-
| `running` | `checkpointed`, `blocked`, `failed` |
|
|
10
|
+
| `running` | `checkpointed`, `blocked`, `failed`, `accepted` |
|
|
11
11
|
| `checkpointed` | `accepted`, `needs_revalidation` |
|
|
12
12
|
| `accepted` | `needs_revalidation` |
|
|
13
13
|
| `blocked` | `pending` |
|
|
@@ -24,6 +24,7 @@ stateDiagram-v2
|
|
|
24
24
|
pending --> blocked : executor 报告阻塞
|
|
25
25
|
|
|
26
26
|
running --> checkpointed : executor 完成 checkpoint
|
|
27
|
+
running --> accepted : L0/review_required=false 自动接受 (跳过 checkpointed)
|
|
27
28
|
running --> blocked : executor 运行时阻塞
|
|
28
29
|
running --> failed : executor 执行失败
|
|
29
30
|
|
|
@@ -172,8 +173,11 @@ stateDiagram-v2
|
|
|
172
173
|
executing_task --> failed : debugger 报告架构问题
|
|
173
174
|
|
|
174
175
|
reviewing_task --> executing_task : 审查完成 (通过或返工)
|
|
175
|
-
reviewing_phase --> executing_task :
|
|
176
|
-
reviewing_phase --> completed : 最终 phase 审查通过
|
|
176
|
+
reviewing_phase --> executing_task : 审查完成 (通过或返工,reviewer 始终返回 executing_task)
|
|
177
|
+
reviewing_phase --> completed : 最终 phase 审查通过 (schema 允许)
|
|
178
|
+
|
|
179
|
+
note right of executing_task : 最终 phase 审查通过后,\nresume 返回 complete_phase action,\nLLM 调用 phase-complete 设置 completed
|
|
180
|
+
executing_task --> completed : phase-complete (最终 phase)
|
|
177
181
|
|
|
178
182
|
awaiting_clear --> executing_task : /clear + /resume 后恢复
|
|
179
183
|
awaiting_user --> executing_task : 用户解除阻塞 / 自动匹配 decision
|
|
@@ -182,14 +186,18 @@ stateDiagram-v2
|
|
|
182
186
|
executing_task --> preflight_overrides : resume 时 preflight 检测
|
|
183
187
|
preflight_overrides --> reconcile_workspace : git HEAD 不匹配
|
|
184
188
|
preflight_overrides --> replan_required : 计划文件被修改
|
|
185
|
-
preflight_overrides --> research_refresh_needed : 研究缓存过期
|
|
186
189
|
preflight_overrides --> awaiting_user : 方向漂移检测
|
|
190
|
+
preflight_overrides --> executing_task : dirty-phase 回滚 (rollback_to_dirty_phase)
|
|
191
|
+
preflight_overrides --> research_refresh_needed : 研究缓存过期
|
|
187
192
|
|
|
188
193
|
research_refresh_needed --> executing_task : 研究刷新完成
|
|
189
194
|
research_refresh_needed --> reviewing_task : 刷新后恢复审查状态
|
|
190
195
|
research_refresh_needed --> reviewing_phase : 刷新后恢复审查状态
|
|
191
196
|
|
|
192
197
|
paused_by_user --> executing_task : 用户恢复
|
|
198
|
+
paused_by_user --> research_refresh_needed : resume 时研究过期
|
|
199
|
+
paused_by_user --> reviewing_task : resume 恢复审查状态
|
|
200
|
+
paused_by_user --> reviewing_phase : resume 恢复审查状态
|
|
193
201
|
|
|
194
202
|
completed --> [*]
|
|
195
203
|
failed --> [*]
|
|
@@ -198,7 +206,8 @@ stateDiagram-v2
|
|
|
198
206
|
### 关键转换说明
|
|
199
207
|
|
|
200
208
|
**执行主路径**:
|
|
201
|
-
`planning -> executing_task -> reviewing_phase -> executing_task (next phase) -> ... -> completed`
|
|
209
|
+
`planning -> executing_task -> reviewing_phase -> executing_task -> complete_phase -> executing_task (next phase) -> ... -> executing_task -> phase-complete -> completed`
|
|
210
|
+
注: `reviewing_phase` 审查通过后始终先回到 `executing_task`,再由 resume 返回 `complete_phase` action,LLM 调用 `phase-complete` MCP tool 推进。最终 phase 的 `phase-complete` 调用会直接设置 `workflow_mode = 'completed'`。
|
|
202
211
|
|
|
203
212
|
**L2 审查分支**:
|
|
204
213
|
`executing_task -> reviewing_task -> executing_task`
|
|
@@ -211,7 +220,8 @@ stateDiagram-v2
|
|
|
211
220
|
1. git HEAD 不匹配 -> `reconcile_workspace`
|
|
212
221
|
2. 计划文件被外部修改 -> `replan_required`
|
|
213
222
|
3. 方向漂移 -> `awaiting_user`
|
|
214
|
-
4.
|
|
223
|
+
4. `current_phase` 之前的 phase 有 `needs_revalidation` task -> `rollback_to_dirty_phase`
|
|
224
|
+
5. 研究缓存过期 -> `research_refresh_needed`
|
|
215
225
|
|
|
216
226
|
**Research 刷新后恢复**:
|
|
217
227
|
`storeResearch()` 中: 如果 `workflow_mode === 'research_refresh_needed'`,调用 `inferWorkflowModeAfterResearch()` 根据 `current_review` 状态推断恢复到 `reviewing_phase` / `reviewing_task` / `executing_task`。
|
package/src/schema.js
CHANGED
|
@@ -602,7 +602,7 @@ export function validateReviewerResult(r) {
|
|
|
602
602
|
if (!(typeof r.scope_id === 'string' || typeof r.scope_id === 'number') || r.scope_id === '' || r.scope_id === 0) {
|
|
603
603
|
errors.push('missing or invalid scope_id');
|
|
604
604
|
}
|
|
605
|
-
if (!['L2', 'L1-batch', 'L1'].includes(r.review_level)) errors.push('invalid review_level (expected L2, L1-batch, or L1)');
|
|
605
|
+
if (!['L3', 'L2', 'L1-batch', 'L1'].includes(r.review_level)) errors.push('invalid review_level (expected L3, L2, L1-batch, or L1)');
|
|
606
606
|
if (typeof r.spec_passed !== 'boolean') errors.push('spec_passed must be boolean');
|
|
607
607
|
if (typeof r.quality_passed !== 'boolean') errors.push('quality_passed must be boolean');
|
|
608
608
|
if (!Array.isArray(r.critical_issues)) errors.push('critical_issues must be array');
|
package/src/server.js
CHANGED
|
@@ -375,7 +375,7 @@ export async function main() {
|
|
|
375
375
|
process.on('SIGINT', () => process.exit(0));
|
|
376
376
|
process.on('SIGTERM', () => process.exit(0));
|
|
377
377
|
process.on('unhandledRejection', (err) => {
|
|
378
|
-
|
|
378
|
+
process.stderr.write(`[gsd] unhandledRejection: ${err?.stack || err}\n`);
|
|
379
379
|
});
|
|
380
380
|
|
|
381
381
|
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
@@ -32,7 +32,7 @@ const RESULT_CONTRACTS = {
|
|
|
32
32
|
reviewer: {
|
|
33
33
|
scope: '"task" | "phase"',
|
|
34
34
|
scope_id: 'string | number — task id (e.g. "1.2") or phase number',
|
|
35
|
-
review_level: '"L2" | "L1-batch" | "L1"',
|
|
35
|
+
review_level: '"L3" | "L2" | "L1-batch" | "L1"',
|
|
36
36
|
spec_passed: 'boolean',
|
|
37
37
|
quality_passed: 'boolean',
|
|
38
38
|
critical_issues: '{ reason|description, task_id?, invalidates_downstream? }[] — blocking issues',
|
|
@@ -361,7 +361,7 @@ function buildErrorFingerprint(result) {
|
|
|
361
361
|
parts.push([...result.files_changed].sort().join(','));
|
|
362
362
|
}
|
|
363
363
|
const combined = parts.filter(Boolean).join('|');
|
|
364
|
-
return combined.length > 0 ? combined.slice(0, 120) : result.summary.slice(0, 80);
|
|
364
|
+
return combined.length > 0 ? combined.slice(0, 120) : (result.summary || '').slice(0, 80);
|
|
365
365
|
}
|
|
366
366
|
|
|
367
367
|
function getBlockedReasonFromResult(result) {
|
|
@@ -376,8 +376,8 @@ function getBlockedReasonFromResult(result) {
|
|
|
376
376
|
};
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
async function persist(basePath, updates, { _append_decisions, _propagation_tasks } = {}) {
|
|
380
|
-
const result = await update({ updates, basePath, _append_decisions, _propagation_tasks });
|
|
379
|
+
async function persist(basePath, updates, { _append_decisions, _propagation_tasks, expectedVersion } = {}) {
|
|
380
|
+
const result = await update({ updates, basePath, expectedVersion, _append_decisions, _propagation_tasks });
|
|
381
381
|
if (result.error) {
|
|
382
382
|
return result;
|
|
383
383
|
}
|
|
@@ -385,8 +385,8 @@ async function persist(basePath, updates, { _append_decisions, _propagation_task
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
// persist variant that returns merged state from update(), avoiding re-reads
|
|
388
|
-
async function persistAndRead(basePath, updates, { _append_decisions, _propagation_tasks } = {}) {
|
|
389
|
-
const result = await update({ updates, basePath, _append_decisions, _propagation_tasks });
|
|
388
|
+
async function persistAndRead(basePath, updates, { _append_decisions, _propagation_tasks, expectedVersion } = {}) {
|
|
389
|
+
const result = await update({ updates, basePath, expectedVersion, _append_decisions, _propagation_tasks });
|
|
390
390
|
if (result.error) {
|
|
391
391
|
return { error: true, ...result };
|
|
392
392
|
}
|
|
@@ -34,7 +34,7 @@ export function setLockPath(lockPath) {
|
|
|
34
34
|
* Must be called before withStateLock in all mutation paths.
|
|
35
35
|
*/
|
|
36
36
|
export function ensureLockPathFromStatePath(statePath) {
|
|
37
|
-
if (
|
|
37
|
+
if (statePath) {
|
|
38
38
|
_fileLockPath = join(dirname(statePath), 'state.lock');
|
|
39
39
|
}
|
|
40
40
|
}
|
|
@@ -44,6 +44,7 @@ export function withStateLock(fn) {
|
|
|
44
44
|
if (_fileLockPath) {
|
|
45
45
|
return withFileLock(_fileLockPath, fn);
|
|
46
46
|
}
|
|
47
|
+
process.stderr.write('[gsd] WARNING: withStateLock called without lock path — cross-process safety not guaranteed\n');
|
|
47
48
|
return fn();
|
|
48
49
|
});
|
|
49
50
|
_mutationQueue = p.catch(() => {});
|
package/src/tools/state/crud.js
CHANGED
|
@@ -606,8 +606,8 @@ export async function addEvidence({ id, data, basePath = process.cwd() }) {
|
|
|
606
606
|
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
|
607
607
|
return { error: true, code: ERROR_CODES.INVALID_INPUT, message: 'data must be a non-null object' };
|
|
608
608
|
}
|
|
609
|
-
if (typeof data.scope !== 'string') {
|
|
610
|
-
return { error: true, code: ERROR_CODES.INVALID_INPUT, message: 'data.scope must be a string' };
|
|
609
|
+
if (typeof data.scope !== 'string' || data.scope.length === 0) {
|
|
610
|
+
return { error: true, code: ERROR_CODES.INVALID_INPUT, message: 'data.scope must be a non-empty string' };
|
|
611
611
|
}
|
|
612
612
|
|
|
613
613
|
const statePath = await getStatePath(basePath);
|
|
@@ -906,7 +906,8 @@ function _applyPatchOp(state, op) {
|
|
|
906
906
|
}
|
|
907
907
|
|
|
908
908
|
case 'update_task': {
|
|
909
|
-
|
|
909
|
+
// Destructure envelope keys explicitly so they don't leak into fields
|
|
910
|
+
const { task_id, task: taskObj, op: _op, phase_id: _pid, ...fields } = op;
|
|
910
911
|
if (typeof task_id !== 'string') return { error: true, message: 'task_id must be a string' };
|
|
911
912
|
|
|
912
913
|
const phase = state.phases.find(p => p.todo?.some(t => t.id === task_id));
|
package/src/tools/state/logic.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Automation/business logic functions
|
|
2
2
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
|
+
import { writeFileSync, unlinkSync } from 'node:fs';
|
|
4
5
|
import { writeFile, rename, unlink } from 'node:fs/promises';
|
|
5
6
|
import { ensureDir, readJson, writeJson, getStatePath } from '../../utils.js';
|
|
6
7
|
import {
|
|
@@ -445,6 +446,12 @@ export async function storeResearch({ result, artifacts, decision_index, basePat
|
|
|
445
446
|
const researchDir = join(gsdDir, 'research');
|
|
446
447
|
await ensureDir(researchDir);
|
|
447
448
|
|
|
449
|
+
// Crash-consistency sentinel: marks the window between artifact renames and
|
|
450
|
+
// state.json write. On recovery (future iteration), presence of this file
|
|
451
|
+
// indicates a potentially inconsistent research state.
|
|
452
|
+
const sentinelPath = join(gsdDir, '.research-commit-pending');
|
|
453
|
+
writeFileSync(sentinelPath, JSON.stringify({ timestamp: Date.now(), pid: process.pid }));
|
|
454
|
+
|
|
448
455
|
// Atomic multi-file write: write all artifacts first, then rename in batch
|
|
449
456
|
const normalizedArtifacts = normalizeResearchArtifacts(artifacts);
|
|
450
457
|
const tmpSuffix = `.${process.pid}-${Date.now()}.tmp`;
|
|
@@ -465,6 +472,7 @@ export async function storeResearch({ result, artifacts, decision_index, basePat
|
|
|
465
472
|
for (const { tmp } of tmpPaths) {
|
|
466
473
|
try { await unlink(tmp); } catch {}
|
|
467
474
|
}
|
|
475
|
+
try { unlinkSync(sentinelPath); } catch {}
|
|
468
476
|
throw err;
|
|
469
477
|
}
|
|
470
478
|
|
|
@@ -501,11 +509,16 @@ export async function storeResearch({ result, artifacts, decision_index, basePat
|
|
|
501
509
|
|
|
502
510
|
const validation = validateState(state);
|
|
503
511
|
if (!validation.valid) {
|
|
512
|
+
try { unlinkSync(sentinelPath); } catch {}
|
|
504
513
|
return { error: true, code: ERROR_CODES.VALIDATION_FAILED, message: `State validation failed: ${validation.errors.join('; ')}` };
|
|
505
514
|
}
|
|
506
515
|
|
|
507
516
|
state._version = (state._version ?? 0) + 1;
|
|
508
517
|
await writeJson(statePath, state);
|
|
518
|
+
|
|
519
|
+
// Remove sentinel after successful state write — crash consistency window closed
|
|
520
|
+
try { unlinkSync(sentinelPath); } catch {}
|
|
521
|
+
|
|
509
522
|
return {
|
|
510
523
|
success: true,
|
|
511
524
|
workflow_mode: state.workflow_mode,
|
package/src/tools/verify.js
CHANGED
|
@@ -29,12 +29,12 @@ function summarizeOutput(output, lines) {
|
|
|
29
29
|
|
|
30
30
|
async function runCommand(command, args, cwd) {
|
|
31
31
|
try {
|
|
32
|
-
const { stdout } = await execFile(command, args, {
|
|
32
|
+
const { stdout, stderr } = await execFile(command, args, {
|
|
33
33
|
cwd,
|
|
34
34
|
encoding: 'utf-8',
|
|
35
35
|
timeout: 120000,
|
|
36
36
|
});
|
|
37
|
-
return { exit_code: 0, summary: summarizeOutput(stdout, 3) };
|
|
37
|
+
return { exit_code: 0, summary: summarizeOutput(stdout || stderr, 3) };
|
|
38
38
|
} catch (err) {
|
|
39
39
|
return {
|
|
40
40
|
exit_code: err.status ?? (typeof err.code === 'number' ? err.code : 1),
|
package/workflows/debugging.md
CHANGED
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
1. 调用 `orchestrator-resume` 获取 action
|
|
132
132
|
2. 按 action 执行对应操作 (见下方 action 处理表)
|
|
133
133
|
3. 操作完成后回到步骤 1
|
|
134
|
-
4. 终止: action ∈ {idle, awaiting_user,
|
|
134
|
+
4. 终止: action ∈ {idle, awaiting_user, noop, phase_failed, task_failed, await_manual_intervention, await_recovery_decision, review_retry_exhausted}
|
|
135
135
|
|
|
136
136
|
不要在循环中间停下来等用户确认 — 让编排器驱动。
|
|
137
137
|
|
|
@@ -151,6 +151,14 @@
|
|
|
151
151
|
| `replan_required` | 计划文件被修改。**自动处理:** 确认计划无误后,调用 `state-update({updates: {workflow_mode: "executing_task"}})` → 继续循环 |
|
|
152
152
|
| `reconcile_workspace` | Git HEAD 不一致。检查变更,调用 `state-update({updates: {git_head: "<当前HEAD>", workflow_mode: "executing_task"}})` → 继续循环 |
|
|
153
153
|
| `rollback_to_dirty_phase` | 早期 phase 有失效 task。**自动处理:** 继续循环 (resume 已回滚 current_phase) |
|
|
154
|
+
| `trigger_review` | 所有 task 已 checkpointed,触发 phase review → 继续循环 (resume 会自动 dispatch_reviewer) |
|
|
155
|
+
| `phase_failed` | debugger 报告架构问题,phase 标记 failed。向用户展示失败信息 |
|
|
156
|
+
| `task_failed` | debugger 报告 task 不可修复 (非架构问题),task 标记 failed。继续循环 (如有其他可运行 task) 或向用户报告 |
|
|
157
|
+
| `review_retry_exhausted` | phase 审查返工次数超限。向用户展示问题,等待用户干预 |
|
|
158
|
+
| `research_stored` | researcher 结果已存储。继续循环 |
|
|
159
|
+
| `awaiting_user` | task 被阻塞或方向漂移,需要用户输入。展示 blockers 列表,等待用户解除 |
|
|
160
|
+
| `await_manual_intervention` | 上下文不足 / 项目暂停 / 计划阶段。根据场景执行: awaiting_clear 时执行 /clear + /resume; paused 时确认恢复; planning 时完成计划并 state-init |
|
|
161
|
+
| `noop` | 工作流已完成 (completed 状态),无需操作。展示完成信息和 PR 建议 |
|
|
154
162
|
| `idle` | 当前 phase 无可运行 task。检查 task 状态和依赖关系,必要时向用户报告 |
|
|
155
163
|
| `await_recovery_decision` | 工作流处于 failed 状态。向用户展示失败信息和恢复选项 (retry/skip/replan) |
|
|
156
164
|
|