gsd-lite 0.6.1 → 0.6.3
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-auto-update.cjs +1 -0
- package/hooks/gsd-session-init.cjs +26 -11
- package/hooks/lib/statusline-composite.cjs +110 -0
- package/install.js +41 -15
- package/package.json +1 -1
- package/src/tools/orchestrator/helpers.js +14 -12
- package/src/tools/orchestrator/resume.js +3 -3
- package/src/tools/orchestrator/reviewer.js +17 -0
- package/src/tools/state/crud.js +24 -7
- package/src/tools/state/logic.js +3 -1
- package/uninstall.js +17 -4
|
@@ -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.3",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
|
@@ -286,6 +286,7 @@ async function fetchLatestRelease(token) {
|
|
|
286
286
|
if (!res.ok) return null;
|
|
287
287
|
|
|
288
288
|
const data = await res.json();
|
|
289
|
+
if (!data.tag_name || !data.tarball_url) return null;
|
|
289
290
|
return {
|
|
290
291
|
version: data.tag_name.replace(/^v/, ''),
|
|
291
292
|
tarballUrl: data.tarball_url,
|
|
@@ -13,10 +13,8 @@ const fs = require('node:fs');
|
|
|
13
13
|
const path = require('node:path');
|
|
14
14
|
const os = require('node:os');
|
|
15
15
|
|
|
16
|
-
const pluginRoot = path.resolve(__dirname, '..');
|
|
17
16
|
const claudeDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
18
17
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
|
19
|
-
const statuslineScript = path.join(pluginRoot, 'hooks', 'gsd-statusline.cjs');
|
|
20
18
|
|
|
21
19
|
// Safety: exit after 4s regardless (hook timeout is 5s)
|
|
22
20
|
setTimeout(() => process.exit(0), 4000).unref();
|
|
@@ -49,21 +47,38 @@ setTimeout(() => process.exit(0), 4000).unref();
|
|
|
49
47
|
} catch { /* silent */ }
|
|
50
48
|
|
|
51
49
|
// ── Phase 2: StatusLine auto-registration ──
|
|
50
|
+
// StatusLine is a top-level settings.json config that the plugin system
|
|
51
|
+
// (hooks.json) cannot manage. Self-heal if not registered.
|
|
52
52
|
try {
|
|
53
|
-
|
|
53
|
+
const stableStatuslinePath = path.join(claudeDir, 'hooks', 'gsd-statusline.cjs');
|
|
54
|
+
if (fs.existsSync(stableStatuslinePath)) {
|
|
54
55
|
let settings = {};
|
|
55
56
|
try {
|
|
56
57
|
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
57
58
|
} catch { /* Can't read settings — skip registration */ }
|
|
58
59
|
|
|
59
|
-
if (settings
|
|
60
|
-
settings.statusLine
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
60
|
+
if (settings) {
|
|
61
|
+
const current = settings.statusLine?.command || '';
|
|
62
|
+
|
|
63
|
+
if (current.includes('gsd-statusline')) {
|
|
64
|
+
// Already registered — nothing to do
|
|
65
|
+
} else if (!current) {
|
|
66
|
+
// No statusLine — register directly
|
|
67
|
+
settings.statusLine = {
|
|
68
|
+
type: 'command',
|
|
69
|
+
command: `node ${JSON.stringify(stableStatuslinePath)}`
|
|
70
|
+
};
|
|
71
|
+
const tmpPath = settingsPath + `.gsd-tmp-${process.pid}`;
|
|
72
|
+
fs.writeFileSync(tmpPath, JSON.stringify(settings, null, 2) + '\n');
|
|
73
|
+
fs.renameSync(tmpPath, settingsPath);
|
|
74
|
+
} else if (current.includes('statusline-composite')) {
|
|
75
|
+
// Composite system (e.g., code-graph) — register as provider
|
|
76
|
+
try {
|
|
77
|
+
const { registerProvider } = require('./lib/statusline-composite.cjs');
|
|
78
|
+
registerProvider(stableStatuslinePath);
|
|
79
|
+
} catch { /* composite helper not available */ }
|
|
80
|
+
}
|
|
81
|
+
// else: some other statusLine, don't overwrite
|
|
67
82
|
}
|
|
68
83
|
}
|
|
69
84
|
} catch { /* silent */ }
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Detect and register with composite statusline systems (e.g., code-graph).
|
|
3
|
+
// Used by install.js, gsd-session-init.cjs, and uninstall.js.
|
|
4
|
+
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const os = require('node:os');
|
|
8
|
+
|
|
9
|
+
// Known composite statusline registry paths
|
|
10
|
+
const REGISTRY_PATHS = [
|
|
11
|
+
path.join(os.homedir(), '.cache', 'code-graph', 'statusline-registry.json'),
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
function isCompositeStatusLine(command) {
|
|
15
|
+
return typeof command === 'string' && command.includes('statusline-composite');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function findCompositeRegistry() {
|
|
19
|
+
for (const p of REGISTRY_PATHS) {
|
|
20
|
+
if (fs.existsSync(p)) return p;
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Register GSD as a provider in the composite statusline registry.
|
|
27
|
+
* Idempotent: updates existing entry or inserts before code-graph.
|
|
28
|
+
* @param {string} statuslineScriptPath - Absolute path to gsd-statusline.cjs
|
|
29
|
+
* @returns {boolean} true if registered/updated
|
|
30
|
+
*/
|
|
31
|
+
function registerProvider(statuslineScriptPath) {
|
|
32
|
+
let registryPath = findCompositeRegistry();
|
|
33
|
+
|
|
34
|
+
// If composite statusLine is configured but registry file is missing,
|
|
35
|
+
// create it if the parent directory exists (e.g., code-graph installed
|
|
36
|
+
// but registry was deleted or not yet created).
|
|
37
|
+
if (!registryPath) {
|
|
38
|
+
for (const candidate of REGISTRY_PATHS) {
|
|
39
|
+
const dir = path.dirname(candidate);
|
|
40
|
+
if (fs.existsSync(dir)) {
|
|
41
|
+
registryPath = candidate;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!registryPath) return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
let registry;
|
|
50
|
+
try {
|
|
51
|
+
registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
52
|
+
} catch {
|
|
53
|
+
registry = []; // File missing or corrupt — start fresh
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(registry)) return false;
|
|
56
|
+
|
|
57
|
+
const command = `node ${JSON.stringify(statuslineScriptPath)}`;
|
|
58
|
+
const provider = { id: 'gsd', command, needsStdin: true };
|
|
59
|
+
|
|
60
|
+
// Find existing GSD entry (by id or command)
|
|
61
|
+
const idx = registry.findIndex(p =>
|
|
62
|
+
p.id === 'gsd' || p.command?.includes('gsd-statusline'));
|
|
63
|
+
|
|
64
|
+
if (idx >= 0) {
|
|
65
|
+
registry[idx] = provider;
|
|
66
|
+
} else {
|
|
67
|
+
// Insert before code-graph for display priority
|
|
68
|
+
const cgIdx = registry.findIndex(p => p.id === 'code-graph');
|
|
69
|
+
if (cgIdx >= 0) registry.splice(cgIdx, 0, provider);
|
|
70
|
+
else registry.unshift(provider);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Atomic write
|
|
74
|
+
const tmp = registryPath + `.${process.pid}-${Date.now()}.tmp`;
|
|
75
|
+
fs.writeFileSync(tmp, JSON.stringify(registry, null, 2) + '\n');
|
|
76
|
+
fs.renameSync(tmp, registryPath);
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Remove GSD entry from composite statusline registry.
|
|
85
|
+
* @returns {boolean} true if an entry was removed
|
|
86
|
+
*/
|
|
87
|
+
function removeProvider() {
|
|
88
|
+
const registryPath = findCompositeRegistry();
|
|
89
|
+
if (!registryPath) return false;
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
|
|
93
|
+
if (!Array.isArray(registry)) return false;
|
|
94
|
+
|
|
95
|
+
const idx = registry.findIndex(p =>
|
|
96
|
+
p.id === 'gsd' || p.command?.includes('gsd-statusline'));
|
|
97
|
+
if (idx < 0) return false;
|
|
98
|
+
|
|
99
|
+
registry.splice(idx, 1);
|
|
100
|
+
|
|
101
|
+
const tmp = registryPath + `.${process.pid}-${Date.now()}.tmp`;
|
|
102
|
+
fs.writeFileSync(tmp, JSON.stringify(registry, null, 2) + '\n');
|
|
103
|
+
fs.renameSync(tmp, registryPath);
|
|
104
|
+
return true;
|
|
105
|
+
} catch {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { isCompositeStatusLine, findCompositeRegistry, registerProvider, removeProvider };
|
package/install.js
CHANGED
|
@@ -11,6 +11,7 @@ import { createRequire } from 'node:module';
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
const _require = createRequire(import.meta.url);
|
|
13
13
|
const { semverSortComparator } = _require('./hooks/lib/semver-sort.cjs');
|
|
14
|
+
const { isCompositeStatusLine, registerProvider: registerCompositeProvider } = _require('./hooks/lib/statusline-composite.cjs');
|
|
14
15
|
const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
|
15
16
|
const RUNTIME_DIR = join(CLAUDE_DIR, 'gsd');
|
|
16
17
|
const DRY_RUN = process.argv.includes('--dry-run');
|
|
@@ -39,16 +40,35 @@ function isInstalledAsPlugin(claudeDir) {
|
|
|
39
40
|
|
|
40
41
|
function registerStatusLine(settings, statuslineScriptPath) {
|
|
41
42
|
const command = `node ${JSON.stringify(statuslineScriptPath)}`;
|
|
42
|
-
|
|
43
|
-
if (settings.statusLine && typeof settings.statusLine === 'object'
|
|
44
|
-
&& !settings.statusLine.command?.includes('gsd-statusline')) {
|
|
45
|
-
log(' ! Preserved existing statusLine');
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
settings.statusLine = { type: 'command', command };
|
|
43
|
+
|
|
49
44
|
// Clean up legacy format (was incorrectly placed in hooks)
|
|
50
45
|
if (settings.hooks?.StatusLine) delete settings.hooks.StatusLine;
|
|
51
|
-
|
|
46
|
+
|
|
47
|
+
const current = settings.statusLine?.command || '';
|
|
48
|
+
|
|
49
|
+
// Already GSD → update command path
|
|
50
|
+
if (current.includes('gsd-statusline')) {
|
|
51
|
+
settings.statusLine = { type: 'command', command };
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// No statusLine → set GSD directly
|
|
56
|
+
if (!current) {
|
|
57
|
+
settings.statusLine = { type: 'command', command };
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Composite statusLine (e.g., code-graph) → register as provider
|
|
62
|
+
if (isCompositeStatusLine(current)) {
|
|
63
|
+
if (registerCompositeProvider(statuslineScriptPath)) {
|
|
64
|
+
log(' ✓ Registered GSD in composite statusLine registry');
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Other statusLine → don't overwrite
|
|
70
|
+
log(' ! Preserved existing statusLine');
|
|
71
|
+
return false;
|
|
52
72
|
}
|
|
53
73
|
|
|
54
74
|
function registerHookEntry(hooks, { hookType, identifier, matcher, timeout }) {
|
|
@@ -170,8 +190,13 @@ export function main() {
|
|
|
170
190
|
copyDir(localNM, join(RUNTIME_DIR, 'node_modules'), 'runtime/node_modules (copied)');
|
|
171
191
|
} else if (!DRY_RUN) {
|
|
172
192
|
log(' ⧗ Installing runtime dependencies...');
|
|
173
|
-
|
|
174
|
-
|
|
193
|
+
try {
|
|
194
|
+
execSync('npm ci --omit=dev', { cwd: RUNTIME_DIR, stdio: 'pipe' });
|
|
195
|
+
log(' ✓ runtime dependencies installed');
|
|
196
|
+
} catch (err) {
|
|
197
|
+
log(` ✗ Failed to install runtime dependencies: ${err.message}`);
|
|
198
|
+
process.exit(1);
|
|
199
|
+
}
|
|
175
200
|
} else {
|
|
176
201
|
log(' [dry-run] Would install runtime dependencies');
|
|
177
202
|
}
|
|
@@ -209,15 +234,16 @@ export function main() {
|
|
|
209
234
|
log(' ✓ MCP server registered in settings.json');
|
|
210
235
|
}
|
|
211
236
|
|
|
212
|
-
//
|
|
213
|
-
//
|
|
237
|
+
// StatusLine is a top-level setting that the plugin system (hooks.json)
|
|
238
|
+
// cannot manage. Always register, regardless of install method.
|
|
239
|
+
const statuslinePath = join(CLAUDE_DIR, 'hooks', 'gsd-statusline.cjs');
|
|
240
|
+
let statusLineRegistered = registerStatusLine(settings, statuslinePath);
|
|
241
|
+
|
|
242
|
+
// Hooks are managed by hooks.json via the plugin system for plugin installs.
|
|
214
243
|
// Only register in settings.json for manual installs to avoid double execution.
|
|
215
|
-
let statusLineRegistered = false;
|
|
216
244
|
let hooksRegistered = false;
|
|
217
245
|
if (!isPluginInstall) {
|
|
218
246
|
if (!settings.hooks) settings.hooks = {};
|
|
219
|
-
const statuslinePath = join(CLAUDE_DIR, 'hooks', 'gsd-statusline.cjs');
|
|
220
|
-
statusLineRegistered = registerStatusLine(settings, statuslinePath);
|
|
221
247
|
for (const config of HOOK_REGISTRY) {
|
|
222
248
|
if (registerHookEntry(settings.hooks, config)) hooksRegistered = true;
|
|
223
249
|
}
|
package/package.json
CHANGED
|
@@ -208,20 +208,11 @@ async function evaluatePreflight(state, basePath) {
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
|
|
212
|
-
if (expired_research.length > 0) {
|
|
213
|
-
hints.push({
|
|
214
|
-
workflow_mode: 'research_refresh_needed',
|
|
215
|
-
action: 'dispatch_researcher',
|
|
216
|
-
updates: { workflow_mode: 'research_refresh_needed' },
|
|
217
|
-
expired_research,
|
|
218
|
-
message: 'Research cache expired and must be refreshed before execution resumes',
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// P0-2: Dirty-phase detection — rollback current_phase to earliest phase
|
|
211
|
+
// Dirty-phase detection — rollback current_phase to earliest phase
|
|
223
212
|
// that has needs_revalidation tasks, ensuring earlier invalidated work
|
|
224
213
|
// is re-executed before proceeding with later phases.
|
|
214
|
+
// Priority: placed before research expiry because dirty-phase rollback is a
|
|
215
|
+
// safety-critical action (prevents executing later phases on stale foundations).
|
|
225
216
|
// Use filter+reduce (not .find) to guarantee lowest-ID match regardless of array order.
|
|
226
217
|
const dirtyPhases = (state.phases || []).filter(p =>
|
|
227
218
|
p.id < state.current_phase
|
|
@@ -245,6 +236,17 @@ async function evaluatePreflight(state, basePath) {
|
|
|
245
236
|
});
|
|
246
237
|
}
|
|
247
238
|
|
|
239
|
+
const expired_research = collectExpiredResearch(state);
|
|
240
|
+
if (expired_research.length > 0) {
|
|
241
|
+
hints.push({
|
|
242
|
+
workflow_mode: 'research_refresh_needed',
|
|
243
|
+
action: 'dispatch_researcher',
|
|
244
|
+
updates: { workflow_mode: 'research_refresh_needed' },
|
|
245
|
+
expired_research,
|
|
246
|
+
message: 'Research cache expired and must be refreshed before execution resumes',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
248
250
|
if (hints.length === 0) return { override: null };
|
|
249
251
|
|
|
250
252
|
return {
|
|
@@ -53,7 +53,7 @@ function _buildResumeSummary(state, response) {
|
|
|
53
53
|
};
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
async function resumeAwaitingClear(state, basePath) {
|
|
56
|
+
async function resumeAwaitingClear(state, basePath, _depth = 0) {
|
|
57
57
|
const health = await readContextHealth(basePath);
|
|
58
58
|
if (health !== null && health < CONTEXT_RESUME_THRESHOLD) {
|
|
59
59
|
const persistError = await persist(basePath, {
|
|
@@ -83,7 +83,7 @@ async function resumeAwaitingClear(state, basePath) {
|
|
|
83
83
|
}
|
|
84
84
|
const persistError = await persist(basePath, updates);
|
|
85
85
|
if (persistError) return persistError;
|
|
86
|
-
return resumeWorkflow({ basePath });
|
|
86
|
+
return resumeWorkflow({ basePath, _depth: _depth + 1 });
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
async function resumeExecutingTask(state, basePath) {
|
|
@@ -319,7 +319,7 @@ export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0, unb
|
|
|
319
319
|
result = await resumeExecutingTask(state, basePath);
|
|
320
320
|
break;
|
|
321
321
|
case 'awaiting_clear':
|
|
322
|
-
result = await resumeAwaitingClear(state, basePath);
|
|
322
|
+
result = await resumeAwaitingClear(state, basePath, _depth);
|
|
323
323
|
break;
|
|
324
324
|
case 'awaiting_user': {
|
|
325
325
|
if (state.current_review?.stage === 'direction_drift') {
|
|
@@ -74,6 +74,23 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
74
74
|
const qualityFailed = result.quality_passed === false;
|
|
75
75
|
const needsRework = hasCritical || specFailed || qualityFailed;
|
|
76
76
|
|
|
77
|
+
// Safety: if rework is needed but no tasks were targeted for rework,
|
|
78
|
+
// fall back to marking all non-accepted checkpointed/accepted tasks as needs_revalidation
|
|
79
|
+
// to prevent infinite review loops (no runnable tasks → trigger_review → same result).
|
|
80
|
+
if (needsRework && taskPatches.filter(p => p.lifecycle === 'needs_revalidation').length === 0) {
|
|
81
|
+
for (const task of (phase.todo || [])) {
|
|
82
|
+
if (task.lifecycle === 'checkpointed' || task.lifecycle === 'accepted') {
|
|
83
|
+
taskPatches.push({
|
|
84
|
+
id: task.id,
|
|
85
|
+
lifecycle: 'needs_revalidation',
|
|
86
|
+
retry_count: 0,
|
|
87
|
+
evidence_refs: [],
|
|
88
|
+
last_review_feedback: ['Reviewer indicated rework needed but did not specify tasks; all completed tasks require revalidation'],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
77
94
|
// Compute retry count once for both exhaustion check and state update
|
|
78
95
|
const currentRetryCount = phase.phase_review?.retry_count || 0;
|
|
79
96
|
const nextRetryCount = needsRework ? currentRetryCount + 1 : 0;
|
package/src/tools/state/crud.js
CHANGED
|
@@ -251,6 +251,11 @@ export async function update({ updates, basePath = process.cwd(), expectedVersio
|
|
|
251
251
|
// Deep merge phases by ID instead of shallow replace [I-1]
|
|
252
252
|
const merged = { ...state, ...updates };
|
|
253
253
|
|
|
254
|
+
// Deep merge context by key (preserves plan_hashes, last_session, etc.)
|
|
255
|
+
if (updates.context && isPlainObject(updates.context)) {
|
|
256
|
+
merged.context = { ...(state.context || {}), ...updates.context };
|
|
257
|
+
}
|
|
258
|
+
|
|
254
259
|
// Deep merge evidence by key (preserves existing entries)
|
|
255
260
|
if (updates.evidence && isPlainObject(updates.evidence)) {
|
|
256
261
|
merged.evidence = { ...(state.evidence || {}), ...updates.evidence };
|
|
@@ -326,10 +331,13 @@ export async function update({ updates, basePath = process.cwd(), expectedVersio
|
|
|
326
331
|
await _pruneEvidenceFromState(merged, merged.current_phase, gsdDir);
|
|
327
332
|
}
|
|
328
333
|
|
|
329
|
-
// Use incremental validation for simple updates (no phases changes)
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
|
|
334
|
+
// Use incremental validation for simple updates (no phases/propagation/decisions changes)
|
|
335
|
+
const needsFullValidation = updates.phases
|
|
336
|
+
|| (_append_decisions?.length > 0)
|
|
337
|
+
|| (_propagation_tasks?.length > 0);
|
|
338
|
+
const validation = needsFullValidation
|
|
339
|
+
? validateState(merged)
|
|
340
|
+
: validateStateUpdate(state, updates);
|
|
333
341
|
if (!validation.valid) {
|
|
334
342
|
return {
|
|
335
343
|
error: true,
|
|
@@ -495,6 +503,7 @@ export async function phaseComplete({
|
|
|
495
503
|
if (!driftValidation.valid) {
|
|
496
504
|
return { error: true, code: ERROR_CODES.VALIDATION_FAILED, message: `Validation failed: ${driftValidation.errors.join('; ')}` };
|
|
497
505
|
}
|
|
506
|
+
state._version = (state._version ?? 0) + 1;
|
|
498
507
|
await writeJson(statePath, state);
|
|
499
508
|
return {
|
|
500
509
|
success: true,
|
|
@@ -525,6 +534,8 @@ export async function phaseComplete({
|
|
|
525
534
|
} else if (state.current_phase === phase_id && phase_id >= state.total_phases) {
|
|
526
535
|
// Final phase completed — mark workflow as completed
|
|
527
536
|
state.workflow_mode = 'completed';
|
|
537
|
+
state.current_task = null;
|
|
538
|
+
state.current_review = null;
|
|
528
539
|
}
|
|
529
540
|
|
|
530
541
|
// Update git_head to current commit
|
|
@@ -539,6 +550,7 @@ export async function phaseComplete({
|
|
|
539
550
|
if (!finalValidation.valid) {
|
|
540
551
|
return { error: true, code: ERROR_CODES.VALIDATION_FAILED, message: `Validation failed: ${finalValidation.errors.join('; ')}` };
|
|
541
552
|
}
|
|
553
|
+
state._version = (state._version ?? 0) + 1;
|
|
542
554
|
await writeJson(statePath, state);
|
|
543
555
|
return { success: true };
|
|
544
556
|
});
|
|
@@ -570,7 +582,7 @@ export async function addEvidence({ id, data, basePath = process.cwd() }) {
|
|
|
570
582
|
if (!result.ok) {
|
|
571
583
|
return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
|
|
572
584
|
}
|
|
573
|
-
const state = result.data;
|
|
585
|
+
const state = migrateState(result.data);
|
|
574
586
|
|
|
575
587
|
if (!state.evidence) {
|
|
576
588
|
state.evidence = {};
|
|
@@ -585,6 +597,7 @@ export async function addEvidence({ id, data, basePath = process.cwd() }) {
|
|
|
585
597
|
await _pruneEvidenceFromState(state, state.current_phase, gsdDir);
|
|
586
598
|
}
|
|
587
599
|
|
|
600
|
+
state._version = (state._version ?? 0) + 1;
|
|
588
601
|
await writeJson(statePath, state);
|
|
589
602
|
return { success: true };
|
|
590
603
|
});
|
|
@@ -650,11 +663,14 @@ export async function pruneEvidence({ currentPhase, basePath = process.cwd() })
|
|
|
650
663
|
if (!result.ok) {
|
|
651
664
|
return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
|
|
652
665
|
}
|
|
653
|
-
const state = result.data;
|
|
666
|
+
const state = migrateState(result.data);
|
|
654
667
|
|
|
655
668
|
const gsdDir = dirname(statePath);
|
|
656
669
|
const archived = await _pruneEvidenceFromState(state, currentPhase, gsdDir);
|
|
657
|
-
if (archived > 0)
|
|
670
|
+
if (archived > 0) {
|
|
671
|
+
state._version = (state._version ?? 0) + 1;
|
|
672
|
+
await writeJson(statePath, state);
|
|
673
|
+
}
|
|
658
674
|
|
|
659
675
|
return { success: true, archived };
|
|
660
676
|
});
|
|
@@ -734,6 +750,7 @@ export async function patchPlan({ operations, basePath = process.cwd() } = {}) {
|
|
|
734
750
|
return { error: true, code: ERROR_CODES.VALIDATION_FAILED, message: `Validation failed: ${validation.errors.join('; ')}` };
|
|
735
751
|
}
|
|
736
752
|
|
|
753
|
+
state._version = (state._version ?? 0) + 1;
|
|
737
754
|
await writeJson(statePath, state);
|
|
738
755
|
return { success: true, applied, plan_version: state.plan_version };
|
|
739
756
|
});
|
package/src/tools/state/logic.js
CHANGED
|
@@ -5,6 +5,7 @@ import { writeFile, rename, unlink } from 'node:fs/promises';
|
|
|
5
5
|
import { ensureDir, readJson, writeJson, getStatePath } from '../../utils.js';
|
|
6
6
|
import {
|
|
7
7
|
TASK_LIFECYCLE,
|
|
8
|
+
migrateState,
|
|
8
9
|
validateResearchArtifacts,
|
|
9
10
|
validateResearchDecisionIndex,
|
|
10
11
|
validateResearcherResult,
|
|
@@ -439,7 +440,7 @@ export async function storeResearch({ result, artifacts, decision_index, basePat
|
|
|
439
440
|
return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: current.error };
|
|
440
441
|
}
|
|
441
442
|
|
|
442
|
-
const state = current.data;
|
|
443
|
+
const state = migrateState(current.data);
|
|
443
444
|
const gsdDir = dirname(statePath);
|
|
444
445
|
const researchDir = join(gsdDir, 'research');
|
|
445
446
|
await ensureDir(researchDir);
|
|
@@ -503,6 +504,7 @@ export async function storeResearch({ result, artifacts, decision_index, basePat
|
|
|
503
504
|
return { error: true, code: ERROR_CODES.VALIDATION_FAILED, message: `State validation failed: ${validation.errors.join('; ')}` };
|
|
504
505
|
}
|
|
505
506
|
|
|
507
|
+
state._version = (state._version ?? 0) + 1;
|
|
506
508
|
await writeJson(statePath, state);
|
|
507
509
|
return {
|
|
508
510
|
success: true,
|
package/uninstall.js
CHANGED
|
@@ -5,6 +5,7 @@ import { existsSync, rmSync, readFileSync, writeFileSync, renameSync } from 'nod
|
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { pathToFileURL } from 'node:url';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
8
9
|
|
|
9
10
|
const CLAUDE_DIR = process.env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
|
|
10
11
|
const RUNTIME_DIR = join(CLAUDE_DIR, 'gsd');
|
|
@@ -27,6 +28,16 @@ function removeDir(path, label) {
|
|
|
27
28
|
export function main() {
|
|
28
29
|
log('GSD-Lite Uninstaller\n');
|
|
29
30
|
|
|
31
|
+
// Clean up GSD entry from composite statusLine registry before removing files
|
|
32
|
+
try {
|
|
33
|
+
const _require = createRequire(import.meta.url);
|
|
34
|
+
const compositeLib = join(CLAUDE_DIR, 'hooks', 'lib', 'statusline-composite.cjs');
|
|
35
|
+
if (existsSync(compositeLib)) {
|
|
36
|
+
const { removeProvider } = _require(compositeLib);
|
|
37
|
+
if (removeProvider()) log(' ✓ Removed GSD from composite statusLine registry');
|
|
38
|
+
}
|
|
39
|
+
} catch { /* best effort */ }
|
|
40
|
+
|
|
30
41
|
log('Removing files...');
|
|
31
42
|
|
|
32
43
|
removeDir(join(CLAUDE_DIR, 'commands', 'gsd'), 'commands/gsd/');
|
|
@@ -49,10 +60,12 @@ export function main() {
|
|
|
49
60
|
const hookLibDir = join(CLAUDE_DIR, 'hooks', 'lib');
|
|
50
61
|
if (existsSync(hookLibDir)) {
|
|
51
62
|
// Only remove GSD-owned files, not other plugins' libs
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
for (const libFile of ['gsd-finder.cjs', 'statusline-composite.cjs', 'semver-sort.cjs']) {
|
|
64
|
+
const fullPath = join(hookLibDir, libFile);
|
|
65
|
+
if (existsSync(fullPath)) {
|
|
66
|
+
rmSync(fullPath);
|
|
67
|
+
log(` ✓ Removed hooks/lib/${libFile}`);
|
|
68
|
+
}
|
|
56
69
|
}
|
|
57
70
|
}
|
|
58
71
|
|