gsd-lite 0.6.4 → 0.6.7

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.6.4",
16
+ "version": "0.6.7",
17
17
  "keywords": [
18
18
  "orchestration",
19
19
  "mcp",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd",
3
- "version": "0.6.4",
3
+ "version": "0.6.7",
4
4
  "description": "AI orchestration tool for Claude Code — GSD management shell + Superpowers quality core",
5
5
  "author": {
6
6
  "name": "sdsrss",
@@ -94,6 +94,7 @@ setTimeout(() => process.exit(0), 4000).unref();
94
94
  if (gsdEntry) {
95
95
  const mcpContent = JSON.stringify({
96
96
  mcpServers: {
97
+ // biome-ignore lint/suspicious/noTemplateCurlyInString: intentional — Claude plugin system substitutes this at runtime
97
98
  gsd: { command: 'node', args: ['${CLAUDE_PLUGIN_ROOT}/launcher.js'] },
98
99
  },
99
100
  }, null, 2) + '\n';
package/hooks/hooks.json CHANGED
@@ -1,41 +1,4 @@
1
1
  {
2
- "description": "GSD-Lite hooks: statusline + context monitor + session lifecycle",
3
- "hooks": {
4
- "SessionStart": [
5
- {
6
- "matcher": "startup",
7
- "hooks": [
8
- {
9
- "type": "command",
10
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-session-init.cjs\"",
11
- "timeout": 5
12
- }
13
- ]
14
- }
15
- ],
16
- "PostToolUse": [
17
- {
18
- "matcher": "*",
19
- "hooks": [
20
- {
21
- "type": "command",
22
- "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/gsd-context-monitor.cjs\"",
23
- "timeout": 3
24
- }
25
- ]
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
- }
39
- ]
40
- }
2
+ "description": "GSD-Lite hooks authoritative registration is in settings.json via install.js. This file cleared to prevent double execution if the plugin system also loads it.",
3
+ "hooks": {}
41
4
  }
package/install.js CHANGED
@@ -21,7 +21,7 @@ const HOOK_FILES = ['gsd-session-init.cjs', 'gsd-auto-update.cjs', 'gsd-context-
21
21
 
22
22
  // Hook registration config: hookType → { file identifier, matcher, timeout? }
23
23
  const HOOK_REGISTRY = [
24
- { hookType: 'SessionStart', identifier: 'gsd-session-init', matcher: 'startup', timeout: 5 },
24
+ { hookType: 'SessionStart', identifier: 'gsd-session-init', matcher: 'startup|clear|compact', timeout: 5 },
25
25
  { hookType: 'PostToolUse', identifier: 'gsd-context-monitor', matcher: '*' },
26
26
  { hookType: 'Stop', identifier: 'gsd-session-stop', matcher: '*', timeout: 3 },
27
27
  ];
@@ -239,33 +239,13 @@ export function main() {
239
239
  const statuslinePath = join(CLAUDE_DIR, 'hooks', 'gsd-statusline.cjs');
240
240
  let statusLineRegistered = registerStatusLine(settings, statuslinePath);
241
241
 
242
- // Hooks are managed by hooks.json via the plugin system for plugin installs.
243
- // Only register in settings.json for manual installs to avoid double execution.
242
+ // Always register hooks in settings.json regardless of install method.
243
+ // The plugin system's hooks.json auto-loading is unreliable settings.json
244
+ // is the only reliable hook registration path (consistent with claude-mem-lite).
244
245
  let hooksRegistered = false;
245
- if (!isPluginInstall) {
246
- if (!settings.hooks) settings.hooks = {};
247
- for (const config of HOOK_REGISTRY) {
248
- if (registerHookEntry(settings.hooks, config)) hooksRegistered = true;
249
- }
250
- } else {
251
- // Clean up stale manual hook entries left from previous install.js runs
252
- if (settings.hooks) {
253
- let cleaned = false;
254
- for (const [hookType, identifier] of [
255
- ['PostToolUse', 'gsd-context-monitor'],
256
- ['SessionStart', 'gsd-session-init'],
257
- ['Stop', 'gsd-session-stop'],
258
- ]) {
259
- if (Array.isArray(settings.hooks[hookType])) {
260
- const before = settings.hooks[hookType].length;
261
- settings.hooks[hookType] = settings.hooks[hookType].filter(e =>
262
- !e.hooks?.some(h => h.command?.includes(identifier)));
263
- if (settings.hooks[hookType].length < before) cleaned = true;
264
- if (settings.hooks[hookType].length === 0) delete settings.hooks[hookType];
265
- }
266
- }
267
- if (cleaned) log(' ✓ Removed stale manual hook entries (plugin hooks.json handles registration)');
268
- }
246
+ if (!settings.hooks) settings.hooks = {};
247
+ for (const config of HOOK_REGISTRY) {
248
+ if (registerHookEntry(settings.hooks, config)) hooksRegistered = true;
269
249
  }
270
250
 
271
251
  const tmpSettings = settingsPath + `.${process.pid}-${Date.now()}.tmp`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-lite",
3
- "version": "0.6.4",
3
+ "version": "0.6.7",
4
4
  "description": "AI orchestration tool for Claude Code — GSD management shell + Superpowers quality core",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,6 +13,7 @@
13
13
  "lint:fix": "biome check --write src/ tests/ hooks/",
14
14
  "start": "node src/server.js",
15
15
  "version": "node scripts/sync-versions.js && git add .claude-plugin/plugin.json .claude-plugin/marketplace.json",
16
+ "prepare": "[ -d .git ] && ln -sf ../../scripts/pre-commit.sh .git/hooks/pre-commit || true",
16
17
  "prepublishOnly": "node scripts/sync-versions.js && npm run lint && npm test"
17
18
  },
18
19
  "repository": {
package/src/server.js CHANGED
@@ -285,12 +285,33 @@ async function dispatchToolCall(name, args) {
285
285
  case 'state-read':
286
286
  result = await read(args || {});
287
287
  break;
288
- case 'state-update':
289
- result = await update(args);
288
+ case 'state-update': {
289
+ const updateResult = await update(args);
290
+ // Strip full state from response to save tokens — keep only _version for optimistic concurrency
291
+ if (updateResult.success && updateResult.state) {
292
+ result = { success: true, _version: updateResult.state._version };
293
+ } else {
294
+ result = updateResult;
295
+ }
290
296
  break;
291
- case 'phase-complete':
292
- result = await phaseComplete(args);
297
+ }
298
+ case 'phase-complete': {
299
+ const pcResult = await phaseComplete(args);
300
+ // Enrich sparse success response with progress info
301
+ if (pcResult.success && !pcResult.action) {
302
+ const st = await read({ fields: ['current_phase', 'total_phases', 'workflow_mode'] });
303
+ result = {
304
+ ...pcResult,
305
+ phase_completed: args.phase_id,
306
+ current_phase: st.current_phase,
307
+ total_phases: st.total_phases,
308
+ workflow_mode: st.workflow_mode,
309
+ };
310
+ } else {
311
+ result = pcResult;
312
+ }
293
313
  break;
314
+ }
294
315
  case 'state-patch':
295
316
  result = await patchPlan(args);
296
317
  break;
@@ -316,9 +337,20 @@ async function dispatchToolCall(name, args) {
316
337
  return result;
317
338
  }
318
339
 
340
+ // Strip result_contract from orchestrator responses — it's static reference data
341
+ // already available in MCP tool descriptions. Saves ~200 tokens per call.
342
+ function stripResultContract(result) {
343
+ if (result && typeof result === 'object' && 'result_contract' in result) {
344
+ const { result_contract, ...rest } = result;
345
+ return rest;
346
+ }
347
+ return result;
348
+ }
349
+
319
350
  export async function handleToolCall(name, args) {
320
351
  try {
321
- return await dispatchToolCall(name, args);
352
+ const result = await dispatchToolCall(name, args);
353
+ return stripResultContract(result);
322
354
  } catch (err) {
323
355
  const message = err instanceof Error ? err.message : String(err);
324
356
  return { error: true, message: `Tool execution failed: ${message}` };
@@ -23,7 +23,7 @@ const RESULT_CONTRACTS = {
23
23
  summary: 'string — non-empty description of work done',
24
24
  checkpoint_commit: 'string — required when outcome="checkpointed"',
25
25
  files_changed: 'string[] — list of modified file paths',
26
- decisions: '{ id, title, rationale }[] — architectural decisions made',
26
+ decisions: '{ id, summary|title, rationale }[] — architectural decisions (summary is canonical; title accepted as alias)',
27
27
  blockers: '{ description, type }[] — what blocked progress (when outcome="blocked")',
28
28
  contract_changed: 'boolean — true if external API/behavior contract changed',
29
29
  confidence: '"high" | "medium" | "low" (optional) — executor self-assessed confidence; affects review level',
@@ -336,12 +336,14 @@ function buildDecisionEntries(decisions, phaseId, taskId, existingCount = 0) {
336
336
  task: taskId,
337
337
  };
338
338
  }
339
- if (decision && typeof decision === 'object' && typeof decision.summary === 'string') {
339
+ if (decision && typeof decision === 'object' && (typeof decision.summary === 'string' || typeof decision.title === 'string')) {
340
+ const summary = decision.summary || decision.title;
340
341
  return {
341
342
  id: decision.id || `decision:${phaseId}:${taskId}:${existingCount + index + 1}`,
342
343
  phase: decision.phase ?? phaseId,
343
344
  task: decision.task ?? taskId,
344
345
  ...decision,
346
+ summary,
345
347
  };
346
348
  }
347
349
  return null;
@@ -216,7 +216,8 @@ async function resumeExecutingTask(state, basePath) {
216
216
  // signal complete_phase instead of going idle
217
217
  const allAccepted = phase.todo.length > 0 && phase.todo.every(t => t.lifecycle === 'accepted');
218
218
  const reviewPassed = phase.phase_review?.status === 'accepted'
219
- || phase.phase_handoff?.required_reviews_passed === true;
219
+ || phase.phase_handoff?.required_reviews_passed === true
220
+ || allAccepted;
220
221
  if (allAccepted && reviewPassed) {
221
222
  // Auto-advance phase lifecycle to 'reviewing' if currently 'active'
222
223
  // (mirrors trigger_review path at line 480-482)
@@ -531,7 +532,7 @@ export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0, unb
531
532
  }
532
533
 
533
534
  // Attach display-ready summary to all successful responses
534
- if (result && result.success && !result.summary) {
535
+ if (result?.success && !result.summary) {
535
536
  const summary = _buildResumeSummary(state, result);
536
537
  // Use the response's workflow_mode if it differs from state (e.g., preflight override)
537
538
  if (result.workflow_mode && result.workflow_mode !== state.workflow_mode) {
@@ -23,6 +23,13 @@ import {
23
23
  } from './constants.js';
24
24
  import { propagateInvalidation } from './logic.js';
25
25
 
26
+ function friendlyReadError(rawError) {
27
+ if (rawError?.includes('ENOENT')) {
28
+ return 'No GSD project found (state.json missing). Run /gsd:start or /gsd:prd to begin.';
29
+ }
30
+ return rawError;
31
+ }
32
+
26
33
  /**
27
34
  * Compute SHA-256 content hashes for an array of file paths.
28
35
  * Returns an object mapping relative-to-gsdDir paths to hex hashes.
@@ -154,7 +161,7 @@ export async function read({ fields, basePath = process.cwd(), validate = false
154
161
 
155
162
  const result = await readJson(statePath);
156
163
  if (!result.ok) {
157
- return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
164
+ return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: friendlyReadError(result.error) };
158
165
  }
159
166
  const state = migrateState(result.data);
160
167
 
@@ -207,7 +214,7 @@ export async function update({ updates, basePath = process.cwd(), expectedVersio
207
214
  return withStateLock(async () => {
208
215
  const result = await readJson(statePath);
209
216
  if (!result.ok) {
210
- return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
217
+ return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: friendlyReadError(result.error) };
211
218
  }
212
219
  const state = migrateState(result.data);
213
220
 
@@ -425,7 +432,7 @@ export async function phaseComplete({
425
432
  return withStateLock(async () => {
426
433
  const result = await readJson(statePath);
427
434
  if (!result.ok) {
428
- return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
435
+ return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: friendlyReadError(result.error) };
429
436
  }
430
437
  const state = migrateState(result.data);
431
438
 
@@ -479,8 +486,11 @@ export async function phaseComplete({
479
486
  };
480
487
  }
481
488
 
489
+ const allTasksAutoAccepted = phase.lifecycle === 'active'
490
+ && phase.todo?.length > 0 && phase.todo.every(t => t.lifecycle === 'accepted');
482
491
  const reviewPassed = phase.phase_review?.status === 'accepted'
483
- || phase.phase_handoff.required_reviews_passed === true;
492
+ || phase.phase_handoff.required_reviews_passed === true
493
+ || allTasksAutoAccepted;
484
494
  if (!reviewPassed) {
485
495
  return {
486
496
  error: true,
@@ -572,7 +582,16 @@ export async function phaseComplete({
572
582
  }
573
583
  state._version = (state._version ?? 0) + 1;
574
584
  await writeJson(statePath, state);
575
- return { success: true };
585
+ const isCompleted = state.workflow_mode === 'completed';
586
+ const nextPhaseInfo = !isCompleted && state.phases.find(p => p.id === state.current_phase);
587
+ return {
588
+ success: true,
589
+ phase_id,
590
+ phase_name: phase.name,
591
+ next_phase: nextPhaseInfo ? { id: nextPhaseInfo.id, name: nextPhaseInfo.name, tasks: nextPhaseInfo.todo?.length || 0 } : null,
592
+ workflow_mode: state.workflow_mode,
593
+ ...(isCompleted ? { message: 'All phases completed — project finished' } : {}),
594
+ };
576
595
  });
577
596
  }
578
597
 
@@ -600,7 +619,7 @@ export async function addEvidence({ id, data, basePath = process.cwd() }) {
600
619
  return withStateLock(async () => {
601
620
  const result = await readJson(statePath);
602
621
  if (!result.ok) {
603
- return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
622
+ return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: friendlyReadError(result.error) };
604
623
  }
605
624
  const state = migrateState(result.data);
606
625
 
@@ -681,7 +700,7 @@ export async function pruneEvidence({ currentPhase, basePath = process.cwd() })
681
700
  return withStateLock(async () => {
682
701
  const result = await readJson(statePath);
683
702
  if (!result.ok) {
684
- return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
703
+ return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: friendlyReadError(result.error) };
685
704
  }
686
705
  const state = migrateState(result.data);
687
706
 
@@ -721,7 +740,7 @@ export async function patchPlan({ operations, basePath = process.cwd() } = {}) {
721
740
  return withStateLock(async () => {
722
741
  const result = await readJson(statePath);
723
742
  if (!result.ok) {
724
- return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
743
+ return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: friendlyReadError(result.error) };
725
744
  }
726
745
  const state = migrateState(result.data);
727
746
 
@@ -887,7 +906,7 @@ function _applyPatchOp(state, op) {
887
906
  }
888
907
 
889
908
  case 'update_task': {
890
- const { task_id, ...fields } = op;
909
+ const { task_id, task: taskObj, ...fields } = op;
891
910
  if (typeof task_id !== 'string') return { error: true, message: 'task_id must be a string' };
892
911
 
893
912
  const phase = state.phases.find(p => p.todo?.some(t => t.id === task_id));
@@ -895,10 +914,12 @@ function _applyPatchOp(state, op) {
895
914
 
896
915
  const task = phase.todo.find(t => t.id === task_id);
897
916
  const allowedFields = ['name', 'level', 'review_required', 'verification_required', 'research_basis'];
917
+ // Support both flat fields and nested task object (consistent with add_task API)
918
+ const source = (taskObj && typeof taskObj === 'object') ? { ...taskObj, ...fields } : fields;
898
919
  const updates = {};
899
920
  for (const key of allowedFields) {
900
- if (key in fields) {
901
- updates[key] = fields[key];
921
+ if (key in source) {
922
+ updates[key] = source[key];
902
923
  }
903
924
  }
904
925