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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/install.js +19 -2
- package/package.json +1 -1
- package/src/schema.js +18 -2
- package/src/server.js +4 -0
- package/src/tools/orchestrator/resume.js +5 -1
- package/src/tools/state/crud.js +12 -1
- package/src/tools/state/logic.js +7 -2
|
@@ -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.
|
|
16
|
+
"version": "0.7.9",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
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
|
-
|
|
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
|
-
|
|
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
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' && !
|
|
796
|
-
|
|
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
|
-
|
|
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') {
|
package/src/tools/state/crud.js
CHANGED
|
@@ -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}` };
|
package/src/tools/state/logic.js
CHANGED
|
@@ -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 —
|
|
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
|
-
|
|
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 = [];
|