gsd-lite 0.7.7 → 0.7.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.
@@ -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.7.7",
16
+ "version": "0.7.9",
17
17
  "keywords": [
18
18
  "orchestration",
19
19
  "mcp",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
4
4
  "description": "AI orchestration tool for Claude Code — GSD management shell + Superpowers quality core",
5
5
  "author": {
6
6
  "name": "sdsrss",
package/install.js CHANGED
@@ -210,7 +210,19 @@ export function main() {
210
210
 
211
211
  // 6. Stable runtime for MCP server
212
212
  copyDir(join(__dirname, 'src'), join(RUNTIME_DIR, 'src'), 'runtime/src → ~/.claude/gsd/src/');
213
- copyFile(join(__dirname, 'package.json'), join(RUNTIME_DIR, 'package.json'), 'runtime/package.json ~/.claude/gsd/package.json');
213
+ // Write a sanitized package.json: strip dev-only npm lifecycle scripts
214
+ // (prepare/prepublishOnly/version use POSIX shell + dev tooling absent from
215
+ // the runtime). Leaving them in means a later manual `npm install` in
216
+ // ~/.claude/gsd fails under cmd.exe on Windows (issue #2).
217
+ if (DRY_RUN) {
218
+ log(' [dry-run] Would write runtime/package.json (dev scripts stripped)');
219
+ } else {
220
+ const runtimePkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf-8'));
221
+ delete runtimePkg.scripts;
222
+ mkdirSync(RUNTIME_DIR, { recursive: true });
223
+ writeFileSync(join(RUNTIME_DIR, 'package.json'), JSON.stringify(runtimePkg, null, 2) + '\n');
224
+ log(' ✓ runtime/package.json → ~/.claude/gsd/package.json (scripts stripped)');
225
+ }
214
226
  // Copy uninstall.js so the SessionStart hook's Phase 0 orphan-cleanup can
215
227
  // spawn it when /plugin uninstall has removed the plugin without running our
216
228
  // uninstaller. Without this, hooks/runtime/settings.json entries written by
@@ -230,7 +242,12 @@ export function main() {
230
242
  log(' ⧗ Installing runtime dependencies...');
231
243
  const lockFile = join(RUNTIME_DIR, 'package-lock.json');
232
244
  const hasLockFile = existsSync(lockFile);
233
- const installCmd = hasLockFile ? 'npm ci --omit=dev' : 'npm install --omit=dev --no-fund --no-audit';
245
+ // --ignore-scripts: the runtime install only needs node_modules. Skipping
246
+ // lifecycle scripts avoids running the dev-only POSIX `prepare` git-hook
247
+ // setup, which fails under cmd.exe on Windows (issue #2).
248
+ const installCmd = hasLockFile
249
+ ? 'npm ci --omit=dev --ignore-scripts'
250
+ : 'npm install --omit=dev --no-fund --no-audit --ignore-scripts';
234
251
  try {
235
252
  execSync(installCmd, { cwd: RUNTIME_DIR, stdio: 'pipe' });
236
253
  log(' ✓ runtime dependencies installed');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-lite",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
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
@@ -761,16 +761,28 @@ export function createInitialState({ project, phases }) {
761
761
  // the public API guard is in init() which rejects phases.length === 0.
762
762
  // Validate task names and uniqueness before creating state
763
763
  const seenIds = new Set();
764
+ // Track task IDs per phase so task-kind dependencies can be checked against the
765
+ // same phase only — selectRunnableTask resolves task deps within phase.todo, so a
766
+ // cross-phase task dep silently deadlocks at runtime. patchPlan.add_dependency
767
+ // rejects the same thing; keep init consistent.
768
+ const phaseTaskIds = phases.map(() => new Set());
764
769
  for (const [pi, p] of phases.entries()) {
765
770
  for (const [ti, t] of (p.tasks || []).entries()) {
766
771
  if (!t.name || typeof t.name !== 'string') {
767
772
  return { error: true, message: `Phase ${pi + 1} task ${ti + 1}: name is required (got ${JSON.stringify(t.name)})` };
768
773
  }
774
+ // Guard explicit index: must be a positive integer or it yields malformed task IDs
775
+ // (e.g. "1.0", "1.1.5", "1.x") that break downstream id parsing.
776
+ if (t.index !== undefined && t.index !== null
777
+ && (!Number.isInteger(t.index) || t.index < 1)) {
778
+ return { error: true, message: `Phase ${pi + 1} task ${ti + 1}: index must be a positive integer (got ${JSON.stringify(t.index)})` };
779
+ }
769
780
  const id = `${pi + 1}.${t.index ?? (ti + 1)}`;
770
781
  if (seenIds.has(id)) {
771
782
  return { error: true, message: `Duplicate task ID: ${id} in phase ${pi + 1}` };
772
783
  }
773
784
  seenIds.add(id);
785
+ phaseTaskIds[pi].add(id);
774
786
  }
775
787
  }
776
788
 
@@ -792,8 +804,12 @@ export function createInitialState({ project, phases }) {
792
804
  if (dep.gate && !validGates.includes(dep.gate)) {
793
805
  return { error: true, message: `Task ${taskId}: requires entry gate must be one of ${validGates.join(', ')} (got "${dep.gate}")` };
794
806
  }
795
- if (dep.kind === 'task' && !seenIds.has(String(dep.id))) {
796
- return { error: true, message: `Task ${taskId}: requires references non-existent task "${dep.id}" (valid IDs: ${[...seenIds].join(', ')})` };
807
+ if (dep.kind === 'task' && !phaseTaskIds[pi].has(String(dep.id))) {
808
+ if (seenIds.has(String(dep.id))) {
809
+ const targetPhase = String(dep.id).split('.')[0];
810
+ return { error: true, message: `Task ${taskId}: requires references task "${dep.id}" in a different phase (cross-phase task dependencies are not supported — use a phase dependency {kind: "phase", id: ${targetPhase}} for cross-phase ordering)` };
811
+ }
812
+ return { error: true, message: `Task ${taskId}: requires references non-existent task "${dep.id}" (same-phase IDs: ${[...phaseTaskIds[pi]].join(', ') || 'none'})` };
797
813
  }
798
814
  if (dep.kind === 'phase') {
799
815
  const phaseId = Number(dep.id);
package/src/server.js CHANGED
@@ -259,6 +259,10 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
259
259
  }));
260
260
 
261
261
  async function dispatchToolCall(name, args) {
262
+ // Normalize missing args (MCP clients may omit `arguments`, sending null/undefined)
263
+ // so every tool receives an object and returns a clean validation error rather than
264
+ // leaking a raw destructuring TypeError.
265
+ args = args || {};
262
266
  let result;
263
267
  switch (name) {
264
268
  case 'health': {
@@ -220,7 +220,11 @@ async function resumeExecutingTask(state, basePath) {
220
220
  const reviewPassed = phase.phase_review?.status === 'accepted'
221
221
  || phase.phase_handoff?.required_reviews_passed === true
222
222
  || allAccepted;
223
- if (allAccepted && reviewPassed) {
223
+ // Zero-task phase (empty milestone) is complete-able once its vacuous review passed —
224
+ // otherwise it can never satisfy the all-tasks-accepted condition and stalls.
225
+ const emptyPhaseDone = phase.todo.length === 0
226
+ && (phase.phase_review?.status === 'accepted' || phase.phase_handoff?.required_reviews_passed === true);
227
+ if ((allAccepted && reviewPassed) || emptyPhaseDone) {
224
228
  // Auto-advance phase lifecycle to 'reviewing' if currently 'active'
225
229
  // (mirrors trigger_review path at line 480-482)
226
230
  if (phase.lifecycle === 'active') {
@@ -60,7 +60,7 @@ export async function computePlanHashes(basePath) {
60
60
  /**
61
61
  * Initialize a new GSD project: creates .gsd/, state.json, plan.md, phases/
62
62
  */
63
- export async function init({ project, phases, research, force = false, basePath = process.cwd() }) {
63
+ export async function init({ project, phases, research, force = false, basePath = process.cwd() } = {}) {
64
64
  if (!project || typeof project !== 'string') {
65
65
  return { error: true, code: ERROR_CODES.INVALID_INPUT, message: 'project must be a non-empty string' };
66
66
  }
@@ -808,6 +808,12 @@ function _applyPatchOp(state, op) {
808
808
  const { phase_id, task } = op;
809
809
  if (typeof phase_id !== 'number') return { error: true, message: 'phase_id must be a number' };
810
810
  if (!task || typeof task.name !== 'string' || task.name.length === 0) return { error: true, message: 'task.name must be a non-empty string' };
811
+ // Guard task.index: a non-positive-integer index produces malformed IDs like
812
+ // "1.0"/"1.1.5"/"1.x"; "1.x" then poisons Math.max() and cascades to "1.NaN".
813
+ if (task.index !== undefined && task.index !== null
814
+ && (!Number.isInteger(task.index) || task.index < 1)) {
815
+ return { error: true, message: `task.index must be a positive integer (got ${JSON.stringify(task.index)})` };
816
+ }
811
817
 
812
818
  const phase = state.phases.find(p => p.id === phase_id);
813
819
  if (!phase) return { error: true, message: `Phase ${phase_id} not found` };
@@ -903,6 +909,11 @@ function _applyPatchOp(state, op) {
903
909
  const existing = new Set(taskMap.keys());
904
910
  const ordered = new Set(order);
905
911
 
912
+ // Reject duplicate IDs explicitly — otherwise a set-size match can slip a
913
+ // duplicate into todo and surface later as a misleading "circular dependency" error.
914
+ if (ordered.size !== order.length) {
915
+ return { error: true, message: `order contains duplicate task IDs for phase ${phase_id}` };
916
+ }
906
917
  // Must contain exactly the same task IDs
907
918
  if (ordered.size !== existing.size || ![...ordered].every(id => existing.has(id))) {
908
919
  return { error: true, message: `order must contain exactly the same task IDs as phase ${phase_id}` };
@@ -36,9 +36,14 @@ export function selectRunnableTask(phase, state, { maxRetry = DEFAULT_MAX_RETRY
36
36
  if (!phase || !Array.isArray(phase.todo)) {
37
37
  return { error: true, message: 'Phase todo must be an array' };
38
38
  }
39
- // D-4: Zero-task phase — immediately trigger review so phase can advance
39
+ // D-4: Zero-task phase — trigger review once so the phase can advance, but only
40
+ // until that (vacuous) review passes. Returning trigger_review unconditionally
41
+ // makes the resume loop oscillate forever (review accepts → resume → review again)
42
+ // because an empty phase never reaches the normal all-accepted completion path.
40
43
  if (phase.todo.length === 0) {
41
- return { mode: 'trigger_review' };
44
+ const reviewPassed = phase.phase_review?.status === 'accepted'
45
+ || phase.phase_handoff?.required_reviews_passed === true;
46
+ return reviewPassed ? { task: undefined, diagnostics: [] } : { mode: 'trigger_review' };
42
47
  }
43
48
 
44
49
  const runnableTasks = [];