gsd-lite 0.5.2 → 0.5.4

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.
@@ -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.2",
16
+ "version": "0.5.4",
17
17
  "keywords": [
18
18
  "orchestration",
19
19
  "mcp",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "AI orchestration tool for Claude Code — GSD management shell + Superpowers quality core",
5
5
  "author": {
6
6
  "name": "sdsrss",
@@ -19,7 +19,7 @@ Check if `.gsd/state.json` exists:
19
19
 
20
20
  ## STEP 2: MCP Server Health
21
21
 
22
- Call the `gsd health` MCP tool:
22
+ Call the `health` MCP tool:
23
23
  - If returns `status: "ok"`: record PASS with server version
24
24
  - If returns error or unreachable: record FAIL with error message
25
25
  - Note: if MCP server is not available at all (tool not found), record FAIL "MCP server not registered"
@@ -51,7 +51,7 @@ Check if `.gsd/.state-lock` exists:
51
51
 
52
52
  Check for update-related information:
53
53
  - Read `~/.claude/gsd/package.json` for installed version
54
- - Compare with the version from `gsd health` tool response
54
+ - Compare with the version from `health` tool response
55
55
  - If versions match: record PASS with version number
56
56
  - If mismatch: record WARN "Version mismatch: installed={x}, server={y}"
57
57
  - If `~/.claude/gsd/.update-pending` exists: record INFO "Update pending, will apply on next session"
package/commands/start.md CHANGED
@@ -12,7 +12,7 @@ argument-hint: Optional feature or project description
12
12
 
13
13
  ## STEP 0 — 已有项目检测
14
14
 
15
- 调用 `gsd health` 工具。如果返回 state_exists=true:
15
+ 调用 `health` 工具(MCP tool 名称: health)。如果返回 state_exists=true:
16
16
  - 告知用户: "检测到进行中的 GSD 项目。"
17
17
  - 提供选项:
18
18
  - (a) 恢复执行 → 转到 `/gsd:resume`
@@ -60,10 +60,11 @@ process.stdin.on('end', () => {
60
60
 
61
61
  // Context window display (USED percentage scaled to usable context)
62
62
  // Claude Code reserves ~16.5% for autocompact buffer (configurable via env)
63
- const AUTO_COMPACT_BUFFER_PCT = Number(process.env.GSD_AUTOCOMPACT_BUFFER) || 16.5;
63
+ const AUTO_COMPACT_BUFFER_PCT = Math.min(99, Math.max(0, Number(process.env.GSD_AUTOCOMPACT_BUFFER) || 16.5));
64
64
  let ctx = '';
65
65
  if (remaining != null) {
66
- const usableRemaining = Math.max(0, ((remaining - AUTO_COMPACT_BUFFER_PCT) / (100 - AUTO_COMPACT_BUFFER_PCT)) * 100);
66
+ const divisor = 100 - AUTO_COMPACT_BUFFER_PCT;
67
+ const usableRemaining = divisor > 0 ? Math.max(0, ((remaining - AUTO_COMPACT_BUFFER_PCT) / divisor) * 100) : 0;
67
68
  const used = Math.max(0, Math.min(100, Math.round(100 - usableRemaining)));
68
69
 
69
70
  // Write bridge file for context-monitor PostToolUse hook (skip if remaining unchanged)
@@ -73,7 +74,7 @@ process.stdin.on('end', () => {
73
74
  let needsWrite = true;
74
75
  try {
75
76
  const existing = JSON.parse(fs.readFileSync(bridgePath, 'utf8'));
76
- if (existing.remaining_percentage === remaining) needsWrite = false;
77
+ if (existing.remaining_percentage === remaining && existing.has_gsd === hasGsd) needsWrite = false;
77
78
  } catch { /* no existing file */ }
78
79
  if (needsWrite) {
79
80
  const tmpBridge = bridgePath + '.tmp';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-lite",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "AI orchestration tool for Claude Code — GSD management shell + Superpowers quality core",
5
5
  "type": "module",
6
6
  "bin": {
package/src/schema.js CHANGED
@@ -17,6 +17,23 @@ export const WORKFLOW_MODES = [
17
17
  'failed',
18
18
  ];
19
19
 
20
+ // Valid workflow_mode transitions — unlisted transitions are rejected by validateStateUpdate.
21
+ // Terminal states (completed, failed) are guarded separately by the FROM-terminal check in state-update.
22
+ export const WORKFLOW_TRANSITIONS = {
23
+ planning: ['executing_task', 'paused_by_user'],
24
+ executing_task: ['planning', 'reviewing_task', 'reviewing_phase', 'awaiting_user', 'awaiting_clear', 'paused_by_user', 'reconcile_workspace', 'replan_required', 'research_refresh_needed', 'failed'],
25
+ reviewing_task: ['executing_task', 'reviewing_phase', 'awaiting_user', 'awaiting_clear', 'paused_by_user', 'failed'],
26
+ reviewing_phase: ['executing_task', 'awaiting_user', 'awaiting_clear', 'paused_by_user', 'completed', 'failed'],
27
+ awaiting_user: ['executing_task', 'reviewing_task', 'reviewing_phase', 'paused_by_user', 'awaiting_clear'],
28
+ awaiting_clear: ['executing_task', 'paused_by_user'],
29
+ paused_by_user: ['executing_task', 'awaiting_user', 'awaiting_clear', 'reconcile_workspace', 'replan_required', 'research_refresh_needed', 'reviewing_task', 'reviewing_phase'],
30
+ reconcile_workspace: ['executing_task', 'paused_by_user'],
31
+ replan_required: ['executing_task', 'paused_by_user'],
32
+ research_refresh_needed: ['executing_task', 'reviewing_task', 'reviewing_phase', 'paused_by_user'],
33
+ completed: [], // terminal — guarded by FROM-terminal check
34
+ failed: [], // terminal — guarded by FROM-terminal check
35
+ };
36
+
20
37
  export const TASK_LIFECYCLE = {
21
38
  pending: ['running', 'blocked'],
22
39
  running: ['checkpointed', 'blocked', 'failed', 'accepted'], // accepted: auto-accept for L0/review_required=false (atomic, skips checkpointed)
@@ -154,11 +171,28 @@ export function validateStateUpdate(state, updates) {
154
171
 
155
172
  for (const key of Object.keys(updates)) {
156
173
  switch (key) {
157
- case 'workflow_mode':
174
+ case 'workflow_mode': {
158
175
  if (!WORKFLOW_MODES.includes(updates.workflow_mode)) {
159
176
  errors.push(`Invalid workflow_mode: ${updates.workflow_mode}`);
177
+ break;
178
+ }
179
+ // Transition whitelist — reject unlisted transitions
180
+ const currentMode = state.workflow_mode;
181
+ if (currentMode && updates.workflow_mode !== currentMode) {
182
+ const allowed = WORKFLOW_TRANSITIONS[currentMode];
183
+ if (allowed && !allowed.includes(updates.workflow_mode)) {
184
+ errors.push(`Invalid workflow_mode transition: '${currentMode}' → '${updates.workflow_mode}' (allowed: ${allowed.join(', ') || 'none (terminal state)'})`);
185
+ }
186
+ }
187
+ // Guard: 'completed' requires all phases accepted
188
+ if (updates.workflow_mode === 'completed' && Array.isArray(state.phases)) {
189
+ const unfinished = state.phases.filter(p => p.lifecycle !== 'accepted');
190
+ if (unfinished.length > 0) {
191
+ errors.push(`Cannot set workflow_mode to 'completed': ${unfinished.length} phase(s) not accepted (${unfinished.map(p => `${p.id}:${p.lifecycle}`).join(', ')})`);
192
+ }
160
193
  }
161
194
  break;
195
+ }
162
196
  case 'current_phase':
163
197
  if (!Number.isFinite(updates.current_phase)) {
164
198
  errors.push('current_phase must be a finite number');
@@ -383,8 +417,12 @@ export function validateState(state) {
383
417
  }
384
418
  }
385
419
  }
386
- // P2-9: workflow_mode consistency — completed project must not have active/running tasks
420
+ // P2-9: workflow_mode consistency — completed project requires all phases accepted
387
421
  if (state.workflow_mode === 'completed' && Array.isArray(state.phases)) {
422
+ const unfinishedPhases = state.phases.filter(p => p.lifecycle !== 'accepted');
423
+ if (unfinishedPhases.length > 0) {
424
+ errors.push(`Completed project has ${unfinishedPhases.length} unfinished phase(s): ${unfinishedPhases.map(p => `${p.id}:${p.lifecycle}`).join(', ')}`);
425
+ }
388
426
  for (const phase of state.phases) {
389
427
  for (const task of (phase.todo || [])) {
390
428
  if (task.lifecycle === 'running') {
@@ -688,6 +726,33 @@ export function createInitialState({ project, phases }) {
688
726
  }
689
727
  }
690
728
 
729
+ // Validate requires references: must be structured objects with valid targets
730
+ for (const [pi, p] of phases.entries()) {
731
+ for (const [ti, t] of (p.tasks || []).entries()) {
732
+ const taskId = `${pi + 1}.${t.index ?? (ti + 1)}`;
733
+ for (const dep of (t.requires || [])) {
734
+ if (typeof dep === 'string') {
735
+ return { error: true, message: `Task ${taskId}: requires entry "${dep}" must be an object {kind: "task"|"phase", id: "..."}, not a string` };
736
+ }
737
+ if (!isPlainObject(dep) || !dep.kind || !dep.id) {
738
+ return { error: true, message: `Task ${taskId}: requires entries must be objects with kind ("task"|"phase") and id` };
739
+ }
740
+ if (!['task', 'phase'].includes(dep.kind)) {
741
+ return { error: true, message: `Task ${taskId}: requires entry kind must be "task" or "phase" (got "${dep.kind}")` };
742
+ }
743
+ if (dep.kind === 'task' && !seenIds.has(String(dep.id))) {
744
+ return { error: true, message: `Task ${taskId}: requires references non-existent task "${dep.id}" (valid IDs: ${[...seenIds].join(', ')})` };
745
+ }
746
+ if (dep.kind === 'phase') {
747
+ const phaseId = Number(dep.id);
748
+ if (!Number.isFinite(phaseId) || phaseId < 1 || phaseId > phases.length) {
749
+ return { error: true, message: `Task ${taskId}: requires references non-existent phase "${dep.id}" (valid: 1-${phases.length})` };
750
+ }
751
+ }
752
+ }
753
+ }
754
+ }
755
+
691
756
  // M-7: Detect circular dependencies within each phase (Kahn's algorithm)
692
757
  for (const [pi, p] of phases.entries()) {
693
758
  const tasks = p.tasks || [];
package/src/server.js CHANGED
@@ -52,7 +52,7 @@ const TOOLS = [
52
52
  name: { type: 'string', description: 'Task name (required)' },
53
53
  index: { type: 'number', description: 'Task index within phase (default: auto)' },
54
54
  level: { type: 'string', description: 'Complexity level: L0/L1/L2/L3 (default: L1)' },
55
- requires: { type: 'array', description: 'Dependency list (default: [])' },
55
+ requires: { type: 'array', description: 'Dependencies: [{kind: "task"|"phase", id: "1.1", gate?: "checkpoint"|"accepted"|"phase_complete"}] (default: [])' },
56
56
  review_required: { type: 'boolean', description: 'Whether review is needed (default: true)' },
57
57
  verification_required: { type: 'boolean', description: 'Whether verification is needed (default: true)' },
58
58
  },
@@ -90,7 +90,7 @@ const TOOLS = [
90
90
  properties: {
91
91
  updates: {
92
92
  type: 'object',
93
- description: 'Key-value pairs of canonical fields to update',
93
+ description: 'Key-value pairs of canonical fields: workflow_mode, current_phase, current_task, current_review, git_head, plan_version, schema_version, total_phases, project, decisions, context, evidence, research',
94
94
  },
95
95
  },
96
96
  required: ['updates'],
@@ -105,7 +105,13 @@ const TOOLS = [
105
105
  phase_id: { type: 'number', description: 'Phase number to complete' },
106
106
  verification: {
107
107
  type: 'object',
108
- description: 'Optional precomputed verification result object with lint/typecheck/test exit codes',
108
+ description: 'Optional precomputed verification all three keys required, exit_code 0 = passed',
109
+ properties: {
110
+ lint: { type: 'object', properties: { exit_code: { type: 'number' } }, required: ['exit_code'] },
111
+ typecheck: { type: 'object', properties: { exit_code: { type: 'number' } }, required: ['exit_code'] },
112
+ test: { type: 'object', properties: { exit_code: { type: 'number' } }, required: ['exit_code'] },
113
+ },
114
+ required: ['lint', 'typecheck', 'test'],
109
115
  },
110
116
  run_verify: {
111
117
  type: 'boolean',
@@ -133,7 +139,10 @@ const TOOLS = [
133
139
  inputSchema: {
134
140
  type: 'object',
135
141
  properties: {
136
- result: { type: 'object', description: 'Executor result payload' },
142
+ result: {
143
+ type: 'object',
144
+ description: 'Executor result: {task_id: string, outcome: "checkpointed"|"blocked"|"failed", summary: string, checkpoint_commit: string|null, files_changed: string[], decisions: [{id, summary, rationale}], blockers: [{description}], contract_changed: boolean, evidence: string[]}',
145
+ },
137
146
  },
138
147
  required: ['result'],
139
148
  },
@@ -144,7 +153,10 @@ const TOOLS = [
144
153
  inputSchema: {
145
154
  type: 'object',
146
155
  properties: {
147
- result: { type: 'object', description: 'Debugger result payload' },
156
+ result: {
157
+ type: 'object',
158
+ description: 'Debugger result: {task_id: string, outcome: "root_cause_found"|"fix_suggested"|"failed", root_cause: string, evidence: object[], hypothesis_tested: [{hypothesis: string, result: "confirmed"|"rejected", evidence: string}], fix_direction: string, fix_attempts: integer, blockers: object[], architecture_concern: boolean}',
159
+ },
148
160
  },
149
161
  required: ['result'],
150
162
  },
@@ -155,9 +167,12 @@ const TOOLS = [
155
167
  inputSchema: {
156
168
  type: 'object',
157
169
  properties: {
158
- result: { type: 'object', description: 'Researcher result payload' },
159
- decision_index: { type: 'object', description: 'Decision index keyed by decision id' },
160
- artifacts: { type: 'object', description: 'Markdown artifact contents keyed by file name' },
170
+ result: {
171
+ type: 'object',
172
+ description: 'Researcher result: {decision_ids: string[], volatility: "low"|"medium"|"high", expires_at: ISO8601 string, sources: [{id: string, type: string, ref: string}]}',
173
+ },
174
+ decision_index: { type: 'object', description: 'Decision index keyed by decision id, each value: {summary: string, source?: string, expires_at?: ISO8601}' },
175
+ artifacts: { type: 'object', description: 'Markdown contents keyed by filename: {STACK.md, ARCHITECTURE.md, PITFALLS.md, SUMMARY.md}' },
161
176
  },
162
177
  required: ['result', 'decision_index', 'artifacts'],
163
178
  },
@@ -168,7 +183,10 @@ const TOOLS = [
168
183
  inputSchema: {
169
184
  type: 'object',
170
185
  properties: {
171
- result: { type: 'object', description: 'Reviewer result payload' },
186
+ result: {
187
+ type: 'object',
188
+ description: 'Reviewer result: {scope: "task"|"phase", scope_id: string|number, review_level: "L2"|"L1-batch", spec_passed: boolean, quality_passed: boolean, critical_issues: object[], important_issues: object[], minor_issues: object[], accepted_tasks: string[], rework_tasks: string[], evidence: object[]}',
189
+ },
172
190
  },
173
191
  required: ['result'],
174
192
  },
@@ -341,10 +341,13 @@ function verificationPassed(verification) {
341
341
 
342
342
  function verificationSummary(verification) {
343
343
  if (!verification || typeof verification !== 'object') return 'no verification details';
344
- return ['lint', 'typecheck', 'test']
345
- .filter((key) => verification[key])
346
- .map((key) => `${key}:${verification[key].exit_code}`)
347
- .join(', ');
344
+ const parts = ['lint', 'typecheck', 'test'].map((key) => {
345
+ const v = verification[key];
346
+ if (!v) return `${key}:missing`;
347
+ if (typeof v !== 'object' || !('exit_code' in v)) return `${key}:invalid-format (expected {exit_code: number})`;
348
+ return `${key}:${v.exit_code === 0 ? 'pass' : `fail(${v.exit_code})`}`;
349
+ });
350
+ return parts.join(', ');
348
351
  }
349
352
 
350
353
  export async function phaseComplete({