gsd-lite 0.6.2 → 0.6.4
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 +26 -11
- package/hooks/lib/statusline-composite.cjs +110 -0
- package/install.js +34 -13
- package/package.json +1 -1
- package/src/schema.js +5 -5
- package/src/server.js +1 -1
- package/src/tools/orchestrator/helpers.js +27 -9
- package/src/tools/state/crud.js +25 -5
- package/src/tools/state/index.js +1 -1
- package/uninstall.js +17 -4
- package/workflows/execution-flow.md +41 -6
|
@@ -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.4",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
|
@@ -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 }) {
|
|
@@ -214,15 +234,16 @@ export function main() {
|
|
|
214
234
|
log(' ✓ MCP server registered in settings.json');
|
|
215
235
|
}
|
|
216
236
|
|
|
217
|
-
//
|
|
218
|
-
//
|
|
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.
|
|
219
243
|
// Only register in settings.json for manual installs to avoid double execution.
|
|
220
|
-
let statusLineRegistered = false;
|
|
221
244
|
let hooksRegistered = false;
|
|
222
245
|
if (!isPluginInstall) {
|
|
223
246
|
if (!settings.hooks) settings.hooks = {};
|
|
224
|
-
const statuslinePath = join(CLAUDE_DIR, 'hooks', 'gsd-statusline.cjs');
|
|
225
|
-
statusLineRegistered = registerStatusLine(settings, statuslinePath);
|
|
226
247
|
for (const config of HOOK_REGISTRY) {
|
|
227
248
|
if (registerHookEntry(settings.hooks, config)) hooksRegistered = true;
|
|
228
249
|
}
|
package/package.json
CHANGED
package/src/schema.js
CHANGED
|
@@ -22,9 +22,9 @@ export const WORKFLOW_MODES = [
|
|
|
22
22
|
export const WORKFLOW_TRANSITIONS = {
|
|
23
23
|
planning: ['executing_task', 'paused_by_user'],
|
|
24
24
|
executing_task: ['planning', 'reviewing_task', 'reviewing_phase', 'awaiting_user', 'awaiting_clear', 'paused_by_user', 'reconcile_workspace', 'replan_required', 'research_refresh_needed', 'failed'],
|
|
25
|
-
reviewing_task: ['executing_task', 'reviewing_phase', 'awaiting_user', 'awaiting_clear', 'paused_by_user', 'failed'],
|
|
26
|
-
reviewing_phase: ['executing_task', 'awaiting_user', 'awaiting_clear', 'paused_by_user', 'completed', 'failed'],
|
|
27
|
-
awaiting_user: ['executing_task', 'reviewing_task', 'reviewing_phase', 'paused_by_user', 'awaiting_clear'],
|
|
25
|
+
reviewing_task: ['executing_task', 'reviewing_phase', 'awaiting_user', 'awaiting_clear', 'paused_by_user', 'reconcile_workspace', 'replan_required', 'failed'],
|
|
26
|
+
reviewing_phase: ['executing_task', 'awaiting_user', 'awaiting_clear', 'paused_by_user', 'reconcile_workspace', 'replan_required', 'completed', 'failed'],
|
|
27
|
+
awaiting_user: ['executing_task', 'reviewing_task', 'reviewing_phase', 'paused_by_user', 'awaiting_clear', 'reconcile_workspace', 'replan_required'],
|
|
28
28
|
awaiting_clear: ['executing_task', 'paused_by_user'],
|
|
29
29
|
paused_by_user: ['executing_task', 'awaiting_user', 'awaiting_clear', 'reconcile_workspace', 'replan_required', 'research_refresh_needed', 'reviewing_task', 'reviewing_phase'],
|
|
30
30
|
reconcile_workspace: ['executing_task', 'paused_by_user'],
|
|
@@ -173,7 +173,7 @@ export function validateStateUpdate(state, updates) {
|
|
|
173
173
|
switch (key) {
|
|
174
174
|
case 'workflow_mode': {
|
|
175
175
|
if (!WORKFLOW_MODES.includes(updates.workflow_mode)) {
|
|
176
|
-
errors.push(`Invalid workflow_mode: ${updates.workflow_mode}`);
|
|
176
|
+
errors.push(`Invalid workflow_mode: ${updates.workflow_mode} (valid: ${WORKFLOW_MODES.join(', ')})`);
|
|
177
177
|
break;
|
|
178
178
|
}
|
|
179
179
|
// Transition whitelist — reject unlisted transitions
|
|
@@ -310,7 +310,7 @@ export function validateState(state) {
|
|
|
310
310
|
errors.push('schema_version must be a finite number');
|
|
311
311
|
}
|
|
312
312
|
if (!WORKFLOW_MODES.includes(state.workflow_mode)) {
|
|
313
|
-
errors.push(`Invalid workflow_mode: ${state.workflow_mode}`);
|
|
313
|
+
errors.push(`Invalid workflow_mode: ${state.workflow_mode} (valid: ${WORKFLOW_MODES.join(', ')})`);
|
|
314
314
|
}
|
|
315
315
|
if (!Number.isFinite(state.plan_version)) {
|
|
316
316
|
errors.push('plan_version must be a finite number');
|
package/src/server.js
CHANGED
|
@@ -119,7 +119,7 @@ const TOOLS = [
|
|
|
119
119
|
properties: {
|
|
120
120
|
updates: {
|
|
121
121
|
type: 'object',
|
|
122
|
-
description: 'Key-value pairs of canonical fields: workflow_mode, current_phase, current_task, current_review, git_head, plan_version, schema_version, total_phases, project, decisions, context, evidence, research',
|
|
122
|
+
description: 'Key-value pairs of canonical fields: workflow_mode (planning|executing_task|reviewing_task|reviewing_phase|awaiting_clear|awaiting_user|paused_by_user|reconcile_workspace|replan_required|research_refresh_needed|completed|failed), current_phase, current_task, current_review, git_head, plan_version, schema_version, total_phases, project, decisions, context, evidence, research',
|
|
123
123
|
},
|
|
124
124
|
},
|
|
125
125
|
required: ['updates'],
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
update,
|
|
6
6
|
buildExecutorContext,
|
|
7
7
|
matchDecisionForBlocker,
|
|
8
|
+
computePlanHashes,
|
|
8
9
|
} from '../state/index.js';
|
|
9
10
|
import { getGitHead, getGsdDir } from '../../utils.js';
|
|
10
11
|
|
|
@@ -173,15 +174,32 @@ async function evaluatePreflight(state, basePath) {
|
|
|
173
174
|
});
|
|
174
175
|
}
|
|
175
176
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
177
|
+
// Plan drift detection — only run when no prior blocking hint (git_head mismatch)
|
|
178
|
+
// exists, because establishing a baseline in a suspect workspace state would
|
|
179
|
+
// anchor hashes to potentially wrong files.
|
|
180
|
+
if (hints.length === 0) {
|
|
181
|
+
const storedHashes = state.context?.plan_hashes;
|
|
182
|
+
if (!storedHashes || Object.keys(storedHashes).length === 0) {
|
|
183
|
+
// No baseline hashes — compute and persist them now (first resume after init).
|
|
184
|
+
// This avoids false drift when state-init creates placeholders that the agent
|
|
185
|
+
// subsequently overwrites with real plan content.
|
|
186
|
+
const freshHashes = await computePlanHashes(basePath);
|
|
187
|
+
if (Object.keys(freshHashes).length > 0) {
|
|
188
|
+
const hashPersistErr = await persist(basePath, { context: { plan_hashes: freshHashes } });
|
|
189
|
+
if (hashPersistErr) return { override: hashPersistErr };
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
const changed_files = await detectPlanDrift(basePath, storedHashes);
|
|
193
|
+
if (changed_files.length > 0) {
|
|
194
|
+
hints.push({
|
|
195
|
+
workflow_mode: 'replan_required',
|
|
196
|
+
action: 'await_manual_intervention',
|
|
197
|
+
updates: { workflow_mode: 'replan_required' },
|
|
198
|
+
changed_files,
|
|
199
|
+
message: 'Plan artifacts changed after the last recorded session',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
185
203
|
}
|
|
186
204
|
|
|
187
205
|
const skipDirectionDrift = state.workflow_mode === 'awaiting_user'
|
package/src/tools/state/crud.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// State CRUD operations
|
|
2
2
|
|
|
3
3
|
import { dirname, join, relative } from 'node:path';
|
|
4
|
-
import { readFile, stat } from 'node:fs/promises';
|
|
4
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
|
-
import { ensureDir, readJson, writeJson, writeAtomic, getStatePath, getGitHead, isPlainObject, clearGsdDirCache } from '../../utils.js';
|
|
6
|
+
import { ensureDir, readJson, writeJson, writeAtomic, getStatePath, getGsdDir, getGitHead, isPlainObject, clearGsdDirCache } from '../../utils.js';
|
|
7
7
|
import {
|
|
8
8
|
CANONICAL_FIELDS,
|
|
9
9
|
TASK_LEVELS,
|
|
@@ -28,7 +28,16 @@ import { propagateInvalidation } from './logic.js';
|
|
|
28
28
|
* Returns an object mapping relative-to-gsdDir paths to hex hashes.
|
|
29
29
|
* Missing/unreadable files are silently skipped.
|
|
30
30
|
*/
|
|
31
|
-
async function computePlanHashes(
|
|
31
|
+
export async function computePlanHashes(basePath) {
|
|
32
|
+
const gsdDir = await getGsdDir(basePath);
|
|
33
|
+
if (!gsdDir) return {};
|
|
34
|
+
const filePaths = [join(gsdDir, 'plan.md')];
|
|
35
|
+
try {
|
|
36
|
+
const phaseFiles = await readdir(join(gsdDir, 'phases'));
|
|
37
|
+
for (const f of phaseFiles) {
|
|
38
|
+
if (f.endsWith('.md')) filePaths.push(join(gsdDir, 'phases', f));
|
|
39
|
+
}
|
|
40
|
+
} catch { /* no phases dir */ }
|
|
32
41
|
const hashes = {};
|
|
33
42
|
for (const filePath of filePaths) {
|
|
34
43
|
try {
|
|
@@ -115,8 +124,9 @@ export async function init({ project, phases, research, force = false, basePath
|
|
|
115
124
|
// Date.toISOString() truncates to milliseconds. Without ceil, the stored timestamp
|
|
116
125
|
// can be slightly less than the file's actual mtime, causing false plan-drift detection.
|
|
117
126
|
state.context.last_session = new Date(Math.ceil(Math.max(...mtimes))).toISOString();
|
|
118
|
-
//
|
|
119
|
-
|
|
127
|
+
// plan_hashes left null — computed lazily on first orchestrator-resume
|
|
128
|
+
// after the agent writes actual plan.md / phases/*.md content (avoids
|
|
129
|
+
// false drift detection from hashing placeholder files).
|
|
120
130
|
await writeJson(statePath, state);
|
|
121
131
|
|
|
122
132
|
return {
|
|
@@ -325,6 +335,16 @@ export async function update({ updates, basePath = process.cwd(), expectedVersio
|
|
|
325
335
|
}
|
|
326
336
|
}
|
|
327
337
|
|
|
338
|
+
// Auto-refresh plan hashes when leaving replan_required — establishes a new
|
|
339
|
+
// baseline so the next resume doesn't re-trigger drift for already-acknowledged changes.
|
|
340
|
+
if (state.workflow_mode === 'replan_required'
|
|
341
|
+
&& updates.workflow_mode && updates.workflow_mode !== 'replan_required') {
|
|
342
|
+
const freshHashes = await computePlanHashes(basePath);
|
|
343
|
+
if (Object.keys(freshHashes).length > 0) {
|
|
344
|
+
merged.context = { ...(merged.context || {}), plan_hashes: freshHashes };
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
328
348
|
// Auto-prune evidence when entries exceed limit
|
|
329
349
|
if (merged.evidence && Object.keys(merged.evidence).length > MAX_EVIDENCE_ENTRIES) {
|
|
330
350
|
const gsdDir = dirname(statePath);
|
package/src/tools/state/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// State module — re-exports all public API
|
|
2
2
|
|
|
3
3
|
export { ERROR_CODES, setLockPath } from './constants.js';
|
|
4
|
-
export { init, read, update, phaseComplete, addEvidence, pruneEvidence, patchPlan } from './crud.js';
|
|
4
|
+
export { init, read, update, phaseComplete, addEvidence, pruneEvidence, patchPlan, computePlanHashes } from './crud.js';
|
|
5
5
|
export { selectRunnableTask, propagateInvalidation, buildExecutorContext, reclassifyReviewLevel, matchDecisionForBlocker, applyResearchRefresh, storeResearch } from './logic.js';
|
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
|
|
|
@@ -129,12 +129,47 @@
|
|
|
129
129
|
|
|
130
130
|
**自动执行循环:** 进入执行后,持续循环直到遇到终止条件:
|
|
131
131
|
1. 调用 `orchestrator-resume` 获取 action
|
|
132
|
-
2. 按 action
|
|
133
|
-
3.
|
|
134
|
-
4.
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
132
|
+
2. 按 action 执行对应操作 (见下方 action 处理表)
|
|
133
|
+
3. 操作完成后回到步骤 1
|
|
134
|
+
4. 终止: action ∈ {idle, awaiting_user, completed, failed, await_manual_intervention}
|
|
135
|
+
|
|
136
|
+
不要在循环中间停下来等用户确认 — 让编排器驱动。
|
|
137
|
+
|
|
138
|
+
**Action 处理表:**
|
|
139
|
+
|
|
140
|
+
| action | 操作 |
|
|
141
|
+
|--------|------|
|
|
142
|
+
| `dispatch_executor` | 派发 `executor` 子代理执行 task → 结果调用 `orchestrator-handle-executor-result` |
|
|
143
|
+
| `dispatch_reviewer` | 派发 `reviewer` 子代理审查 → 结果调用 `orchestrator-handle-reviewer-result` |
|
|
144
|
+
| `dispatch_debugger` | 派发 `debugger` 子代理调试 → 结果调用 `orchestrator-handle-debugger-result` |
|
|
145
|
+
| `dispatch_researcher` | 派发 `researcher` 子代理研究 → 结果调用 `orchestrator-handle-researcher-result` |
|
|
146
|
+
| `retry_executor` | 重新派发 executor (带 retry 上下文),同 dispatch_executor |
|
|
147
|
+
| `complete_phase` | 调用 `phase-complete` MCP tool (参数见下方) → 自动推进下一 phase |
|
|
148
|
+
| `rework_required` | 有 task 需要返工 → 继续循环 (resume 会自动选择返工 task) |
|
|
149
|
+
| `review_accepted` | 审查通过 → 继续循环 |
|
|
150
|
+
| `continue_execution` | L0/auto-accept 后 → 继续循环 |
|
|
151
|
+
| `replan_required` | 计划文件被修改。**自动处理:** 确认计划无误后,调用 `state-update({updates: {workflow_mode: "executing_task"}})` → 继续循环 |
|
|
152
|
+
| `reconcile_workspace` | Git HEAD 不一致。检查变更,调用 `state-update({updates: {git_head: "<当前HEAD>", workflow_mode: "executing_task"}})` → 继续循环 |
|
|
153
|
+
| `rollback_to_dirty_phase` | 早期 phase 有失效 task。**自动处理:** 继续循环 (resume 已回滚 current_phase) |
|
|
154
|
+
| `idle` | 当前 phase 无可运行 task。检查 task 状态和依赖关系,必要时向用户报告 |
|
|
155
|
+
| `await_recovery_decision` | 工作流处于 failed 状态。向用户展示失败信息和恢复选项 (retry/skip/replan) |
|
|
156
|
+
|
|
157
|
+
**`phase-complete` 参数:**
|
|
158
|
+
```
|
|
159
|
+
phase-complete({
|
|
160
|
+
phase_id: <当前 phase 编号>,
|
|
161
|
+
run_verify: true, // 自动运行 lint/typecheck/test
|
|
162
|
+
direction_ok: true // 方向校验通过 (如有偏差设为 false)
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
如果没有 lint/typecheck/test 工具,可改用 `verification` 参数传入预计算结果:
|
|
166
|
+
```
|
|
167
|
+
phase-complete({
|
|
168
|
+
phase_id: <phase>,
|
|
169
|
+
verification: { lint: {exit_code: 0}, typecheck: {exit_code: 0}, test: {exit_code: 0} },
|
|
170
|
+
direction_ok: true
|
|
171
|
+
})
|
|
172
|
+
```
|
|
138
173
|
</execution_loop>
|
|
139
174
|
|
|
140
175
|
## STEP 12 — 最终报告
|