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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/gsd-session-init.cjs +1 -0
- package/hooks/hooks.json +2 -39
- package/install.js +7 -27
- package/package.json +2 -1
- package/src/server.js +37 -5
- package/src/tools/orchestrator/helpers.js +4 -2
- package/src/tools/orchestrator/resume.js +3 -2
- package/src/tools/state/crud.js +32 -11
|
@@ -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.7",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
|
@@ -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
|
|
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
|
-
//
|
|
243
|
-
//
|
|
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 (!
|
|
246
|
-
|
|
247
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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) {
|
package/src/tools/state/crud.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
901
|
-
updates[key] =
|
|
921
|
+
if (key in source) {
|
|
922
|
+
updates[key] = source[key];
|
|
902
923
|
}
|
|
903
924
|
}
|
|
904
925
|
|