gsd-lite 0.2.1 → 0.3.2
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 +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.mcp.json +3 -3
- package/README.md +7 -6
- package/agents/{gsd-debugger.md → debugger.md} +2 -2
- package/agents/{gsd-executor.md → executor.md} +2 -2
- package/agents/{gsd-researcher.md → researcher.md} +2 -2
- package/agents/{gsd-reviewer.md → reviewer.md} +2 -2
- package/cli.js +5 -5
- package/commands/prd.md +291 -0
- package/commands/{gsd-resume.md → resume.md} +7 -8
- package/commands/{gsd-start.md → start.md} +9 -10
- package/commands/{gsd-status.md → status.md} +0 -1
- package/commands/{gsd-stop.md → stop.md} +0 -1
- package/hooks/context-monitor.js +8 -28
- package/hooks/gsd-context-monitor.cjs +124 -0
- package/hooks/gsd-session-init.cjs +61 -0
- package/hooks/gsd-statusline.cjs +114 -0
- package/hooks/hooks.json +15 -2
- package/install.js +35 -22
- package/launcher.js +25 -0
- package/package.json +4 -3
- package/references/questioning.md +1 -1
- package/src/schema.js +29 -5
- package/src/server.js +45 -25
- package/src/tools/orchestrator.js +86 -51
- package/src/tools/state.js +29 -18
- package/src/tools/verify.js +10 -6
- package/src/utils.js +39 -18
- package/uninstall.js +84 -22
- package/workflows/debugging.md +1 -1
- package/workflows/deviation-rules.md +1 -1
- package/workflows/research.md +1 -1
- package/workflows/review-cycle.md +1 -1
- package/workflows/tdd-cycle.md +1 -1
- package/commands/gsd-prd.md +0 -154
package/src/server.js
CHANGED
|
@@ -2,7 +2,11 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
2
2
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
4
|
import { pathToFileURL } from 'node:url';
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
5
6
|
import { init, read, update, phaseComplete } from './tools/state.js';
|
|
7
|
+
|
|
8
|
+
const _require = createRequire(import.meta.url);
|
|
9
|
+
const PKG_VERSION = _require('../package.json').version;
|
|
6
10
|
import {
|
|
7
11
|
handleDebuggerResult,
|
|
8
12
|
handleExecutorResult,
|
|
@@ -12,13 +16,13 @@ import {
|
|
|
12
16
|
} from './tools/orchestrator.js';
|
|
13
17
|
|
|
14
18
|
const server = new Server(
|
|
15
|
-
{ name: 'gsd
|
|
19
|
+
{ name: 'gsd', version: PKG_VERSION },
|
|
16
20
|
{ capabilities: { tools: {} } }
|
|
17
21
|
);
|
|
18
22
|
|
|
19
23
|
const TOOLS = [
|
|
20
24
|
{
|
|
21
|
-
name: '
|
|
25
|
+
name: 'health',
|
|
22
26
|
description: 'Health check: returns server status and whether .gsd state exists',
|
|
23
27
|
inputSchema: {
|
|
24
28
|
type: 'object',
|
|
@@ -26,7 +30,7 @@ const TOOLS = [
|
|
|
26
30
|
},
|
|
27
31
|
},
|
|
28
32
|
{
|
|
29
|
-
name: '
|
|
33
|
+
name: 'state-init',
|
|
30
34
|
description: 'Initialize .gsd/ directory with state.json, plan.md, and phases/*.md',
|
|
31
35
|
inputSchema: {
|
|
32
36
|
type: 'object',
|
|
@@ -38,9 +42,25 @@ const TOOLS = [
|
|
|
38
42
|
items: {
|
|
39
43
|
type: 'object',
|
|
40
44
|
properties: {
|
|
41
|
-
name: { type: 'string' },
|
|
42
|
-
tasks: {
|
|
45
|
+
name: { type: 'string', description: 'Phase name' },
|
|
46
|
+
tasks: {
|
|
47
|
+
type: 'array',
|
|
48
|
+
description: 'Task definitions',
|
|
49
|
+
items: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
name: { type: 'string', description: 'Task name (required)' },
|
|
53
|
+
index: { type: 'number', description: 'Task index within phase (default: auto)' },
|
|
54
|
+
level: { type: 'string', description: 'Complexity level: L0/L1/L2/L3 (default: L1)' },
|
|
55
|
+
requires: { type: 'array', description: 'Dependency list (default: [])' },
|
|
56
|
+
review_required: { type: 'boolean', description: 'Whether review is needed (default: true)' },
|
|
57
|
+
verification_required: { type: 'boolean', description: 'Whether verification is needed (default: true)' },
|
|
58
|
+
},
|
|
59
|
+
required: ['name'],
|
|
60
|
+
},
|
|
61
|
+
},
|
|
43
62
|
},
|
|
63
|
+
required: ['name'],
|
|
44
64
|
},
|
|
45
65
|
},
|
|
46
66
|
research: { type: 'boolean', description: 'Whether research directory is needed' },
|
|
@@ -49,7 +69,7 @@ const TOOLS = [
|
|
|
49
69
|
},
|
|
50
70
|
},
|
|
51
71
|
{
|
|
52
|
-
name: '
|
|
72
|
+
name: 'state-read',
|
|
53
73
|
description: 'Read state.json, optionally filtering to specific fields',
|
|
54
74
|
inputSchema: {
|
|
55
75
|
type: 'object',
|
|
@@ -63,7 +83,7 @@ const TOOLS = [
|
|
|
63
83
|
},
|
|
64
84
|
},
|
|
65
85
|
{
|
|
66
|
-
name: '
|
|
86
|
+
name: 'state-update',
|
|
67
87
|
description: 'Update state.json canonical fields with lifecycle validation',
|
|
68
88
|
inputSchema: {
|
|
69
89
|
type: 'object',
|
|
@@ -77,7 +97,7 @@ const TOOLS = [
|
|
|
77
97
|
},
|
|
78
98
|
},
|
|
79
99
|
{
|
|
80
|
-
name: '
|
|
100
|
+
name: 'phase-complete',
|
|
81
101
|
description: 'Mark a phase as complete after verifying handoff gate conditions',
|
|
82
102
|
inputSchema: {
|
|
83
103
|
type: 'object',
|
|
@@ -100,7 +120,7 @@ const TOOLS = [
|
|
|
100
120
|
},
|
|
101
121
|
},
|
|
102
122
|
{
|
|
103
|
-
name: '
|
|
123
|
+
name: 'orchestrator-resume',
|
|
104
124
|
description: 'Resume the minimal orchestration loop from workflow_mode/current_phase state',
|
|
105
125
|
inputSchema: {
|
|
106
126
|
type: 'object',
|
|
@@ -108,7 +128,7 @@ const TOOLS = [
|
|
|
108
128
|
},
|
|
109
129
|
},
|
|
110
130
|
{
|
|
111
|
-
name: '
|
|
131
|
+
name: 'orchestrator-handle-executor-result',
|
|
112
132
|
description: 'Persist an executor result and determine the next orchestration action',
|
|
113
133
|
inputSchema: {
|
|
114
134
|
type: 'object',
|
|
@@ -119,7 +139,7 @@ const TOOLS = [
|
|
|
119
139
|
},
|
|
120
140
|
},
|
|
121
141
|
{
|
|
122
|
-
name: '
|
|
142
|
+
name: 'orchestrator-handle-debugger-result',
|
|
123
143
|
description: 'Persist a debugger result and determine the next orchestration action',
|
|
124
144
|
inputSchema: {
|
|
125
145
|
type: 'object',
|
|
@@ -130,7 +150,7 @@ const TOOLS = [
|
|
|
130
150
|
},
|
|
131
151
|
},
|
|
132
152
|
{
|
|
133
|
-
name: '
|
|
153
|
+
name: 'orchestrator-handle-researcher-result',
|
|
134
154
|
description: 'Persist a researcher result, write .gsd/research artifacts, and continue orchestration',
|
|
135
155
|
inputSchema: {
|
|
136
156
|
type: 'object',
|
|
@@ -143,7 +163,7 @@ const TOOLS = [
|
|
|
143
163
|
},
|
|
144
164
|
},
|
|
145
165
|
{
|
|
146
|
-
name: '
|
|
166
|
+
name: 'orchestrator-handle-reviewer-result',
|
|
147
167
|
description: 'Persist a reviewer result, update task lifecycles, and determine next orchestration action',
|
|
148
168
|
inputSchema: {
|
|
149
169
|
type: 'object',
|
|
@@ -162,12 +182,12 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
162
182
|
async function dispatchToolCall(name, args) {
|
|
163
183
|
let result;
|
|
164
184
|
switch (name) {
|
|
165
|
-
case '
|
|
185
|
+
case 'health': {
|
|
166
186
|
const stateResult = await read(args || {});
|
|
167
187
|
result = {
|
|
168
188
|
status: 'ok',
|
|
169
|
-
server: 'gsd
|
|
170
|
-
version:
|
|
189
|
+
server: 'gsd',
|
|
190
|
+
version: PKG_VERSION,
|
|
171
191
|
state_exists: !stateResult.error,
|
|
172
192
|
...(stateResult.error ? {} : {
|
|
173
193
|
project: stateResult.project,
|
|
@@ -178,31 +198,31 @@ async function dispatchToolCall(name, args) {
|
|
|
178
198
|
};
|
|
179
199
|
break;
|
|
180
200
|
}
|
|
181
|
-
case '
|
|
201
|
+
case 'state-init':
|
|
182
202
|
result = await init(args);
|
|
183
203
|
break;
|
|
184
|
-
case '
|
|
204
|
+
case 'state-read':
|
|
185
205
|
result = await read(args || {});
|
|
186
206
|
break;
|
|
187
|
-
case '
|
|
207
|
+
case 'state-update':
|
|
188
208
|
result = await update(args);
|
|
189
209
|
break;
|
|
190
|
-
case '
|
|
210
|
+
case 'phase-complete':
|
|
191
211
|
result = await phaseComplete(args);
|
|
192
212
|
break;
|
|
193
|
-
case '
|
|
213
|
+
case 'orchestrator-resume':
|
|
194
214
|
result = await resumeWorkflow(args || {});
|
|
195
215
|
break;
|
|
196
|
-
case '
|
|
216
|
+
case 'orchestrator-handle-executor-result':
|
|
197
217
|
result = await handleExecutorResult(args || {});
|
|
198
218
|
break;
|
|
199
|
-
case '
|
|
219
|
+
case 'orchestrator-handle-debugger-result':
|
|
200
220
|
result = await handleDebuggerResult(args || {});
|
|
201
221
|
break;
|
|
202
|
-
case '
|
|
222
|
+
case 'orchestrator-handle-researcher-result':
|
|
203
223
|
result = await handleResearcherResult(args || {});
|
|
204
224
|
break;
|
|
205
|
-
case '
|
|
225
|
+
case 'orchestrator-handle-reviewer-result':
|
|
206
226
|
result = await handleReviewerResult(args || {});
|
|
207
227
|
break;
|
|
208
228
|
default:
|
|
@@ -27,7 +27,7 @@ function parseTimestamp(value) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
async function readContextHealth(basePath) {
|
|
30
|
-
const gsdDir = getGsdDir(basePath);
|
|
30
|
+
const gsdDir = await getGsdDir(basePath);
|
|
31
31
|
if (!gsdDir) return null;
|
|
32
32
|
try {
|
|
33
33
|
const raw = await readFile(join(gsdDir, '.context-health'), 'utf-8');
|
|
@@ -75,7 +75,7 @@ async function detectPlanDrift(basePath, lastSession) {
|
|
|
75
75
|
const lastSessionTs = parseTimestamp(lastSession);
|
|
76
76
|
if (lastSessionTs === null) return [];
|
|
77
77
|
|
|
78
|
-
const gsdDir = getGsdDir(basePath);
|
|
78
|
+
const gsdDir = await getGsdDir(basePath);
|
|
79
79
|
if (!gsdDir) return [];
|
|
80
80
|
|
|
81
81
|
const candidates = [join(gsdDir, 'plan.md')];
|
|
@@ -106,41 +106,37 @@ async function evaluatePreflight(state, basePath) {
|
|
|
106
106
|
return { override: null };
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
const
|
|
109
|
+
const hints = [];
|
|
110
|
+
|
|
111
|
+
const currentGitHead = await getGitHead(basePath);
|
|
110
112
|
if (state.git_head && currentGitHead && state.git_head !== currentGitHead) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
},
|
|
120
|
-
};
|
|
113
|
+
hints.push({
|
|
114
|
+
workflow_mode: 'reconcile_workspace',
|
|
115
|
+
action: 'await_manual_intervention',
|
|
116
|
+
updates: { workflow_mode: 'reconcile_workspace' },
|
|
117
|
+
saved_git_head: state.git_head,
|
|
118
|
+
current_git_head: currentGitHead,
|
|
119
|
+
message: 'Saved git_head does not match the current workspace HEAD',
|
|
120
|
+
});
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
const changed_files = await detectPlanDrift(basePath, state.context?.last_session);
|
|
124
124
|
if (changed_files.length > 0) {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (state.workflow_mode === 'awaiting_user' && state.current_review?.stage === 'direction_drift') {
|
|
137
|
-
return { override: null };
|
|
125
|
+
hints.push({
|
|
126
|
+
workflow_mode: 'replan_required',
|
|
127
|
+
action: 'await_manual_intervention',
|
|
128
|
+
updates: { workflow_mode: 'replan_required' },
|
|
129
|
+
changed_files,
|
|
130
|
+
message: 'Plan artifacts changed after the last recorded session',
|
|
131
|
+
});
|
|
138
132
|
}
|
|
139
133
|
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
134
|
+
const skipDirectionDrift = state.workflow_mode === 'awaiting_user'
|
|
135
|
+
&& state.current_review?.stage === 'direction_drift';
|
|
136
|
+
if (!skipDirectionDrift) {
|
|
137
|
+
const driftPhase = getDirectionDriftPhase(state);
|
|
138
|
+
if (driftPhase) {
|
|
139
|
+
hints.push({
|
|
144
140
|
workflow_mode: 'awaiting_user',
|
|
145
141
|
action: 'awaiting_user',
|
|
146
142
|
updates: {
|
|
@@ -155,24 +151,27 @@ async function evaluatePreflight(state, basePath) {
|
|
|
155
151
|
},
|
|
156
152
|
drift_phase: { id: driftPhase.id, name: driftPhase.name },
|
|
157
153
|
message: `Direction drift detected for phase ${driftPhase.id}; user decision required before resuming`,
|
|
158
|
-
}
|
|
159
|
-
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
160
156
|
}
|
|
161
157
|
|
|
162
158
|
const expired_research = collectExpiredResearch(state);
|
|
163
159
|
if (expired_research.length > 0) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
},
|
|
172
|
-
};
|
|
160
|
+
hints.push({
|
|
161
|
+
workflow_mode: 'research_refresh_needed',
|
|
162
|
+
action: 'dispatch_researcher',
|
|
163
|
+
updates: { workflow_mode: 'research_refresh_needed' },
|
|
164
|
+
expired_research,
|
|
165
|
+
message: 'Research cache expired and must be refreshed before execution resumes',
|
|
166
|
+
});
|
|
173
167
|
}
|
|
174
168
|
|
|
175
|
-
return { override: null };
|
|
169
|
+
if (hints.length === 0) return { override: null };
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
override: hints[0],
|
|
173
|
+
hints: hints.length > 1 ? hints.map(h => h.message) : undefined,
|
|
174
|
+
};
|
|
176
175
|
}
|
|
177
176
|
|
|
178
177
|
function getCurrentPhase(state) {
|
|
@@ -386,6 +385,7 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
386
385
|
if (state.current_task) {
|
|
387
386
|
const currentTask = getTaskById(phase, state.current_task);
|
|
388
387
|
if (currentTask?.lifecycle === 'running') {
|
|
388
|
+
const isRetrying = (currentTask.retry_count || 0) > 0;
|
|
389
389
|
const persistError = await persist(basePath, {
|
|
390
390
|
workflow_mode: 'executing_task',
|
|
391
391
|
current_task: currentTask.id,
|
|
@@ -394,7 +394,12 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
394
394
|
if (persistError) return persistError;
|
|
395
395
|
return buildExecutorDispatch(state, phase, currentTask, {
|
|
396
396
|
resumed: true,
|
|
397
|
-
interruption_recovered:
|
|
397
|
+
interruption_recovered: !isRetrying,
|
|
398
|
+
...(isRetrying ? {
|
|
399
|
+
retry_after_failure: true,
|
|
400
|
+
retry_count: currentTask.retry_count,
|
|
401
|
+
last_failure_summary: currentTask.last_failure_summary,
|
|
402
|
+
} : {}),
|
|
398
403
|
});
|
|
399
404
|
}
|
|
400
405
|
}
|
|
@@ -419,11 +424,16 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
419
424
|
|
|
420
425
|
if (selection.mode === 'trigger_review') {
|
|
421
426
|
const current_review = { scope: 'phase', scope_id: phase.id };
|
|
422
|
-
const
|
|
427
|
+
const updates = {
|
|
423
428
|
workflow_mode: 'reviewing_phase',
|
|
424
429
|
current_task: null,
|
|
425
430
|
current_review,
|
|
426
|
-
}
|
|
431
|
+
};
|
|
432
|
+
// Auto-advance phase lifecycle to 'reviewing' if currently 'active'
|
|
433
|
+
if (phase.lifecycle === 'active') {
|
|
434
|
+
updates.phases = [{ id: phase.id, lifecycle: 'reviewing' }];
|
|
435
|
+
}
|
|
436
|
+
const persistError = await persist(basePath, updates);
|
|
427
437
|
if (persistError) return persistError;
|
|
428
438
|
|
|
429
439
|
return {
|
|
@@ -492,6 +502,7 @@ export async function resumeWorkflow({ basePath = process.cwd() } = {}) {
|
|
|
492
502
|
...(preflight.override.current_git_head ? { current_git_head: preflight.override.current_git_head } : {}),
|
|
493
503
|
...(preflight.override.changed_files ? { changed_files: preflight.override.changed_files } : {}),
|
|
494
504
|
...(preflight.override.expired_research ? { expired_research: preflight.override.expired_research } : {}),
|
|
505
|
+
...(preflight.hints ? { pending_issues: preflight.hints } : {}),
|
|
495
506
|
};
|
|
496
507
|
}
|
|
497
508
|
|
|
@@ -562,6 +573,7 @@ export async function resumeWorkflow({ basePath = process.cwd() } = {}) {
|
|
|
562
573
|
id: task.id,
|
|
563
574
|
level: task.level,
|
|
564
575
|
checkpoint_commit: task.checkpoint_commit || null,
|
|
576
|
+
files_changed: task.files_changed || [],
|
|
565
577
|
})),
|
|
566
578
|
};
|
|
567
579
|
}
|
|
@@ -688,8 +700,9 @@ export async function handleExecutorResult({ result, basePath = process.cwd() }
|
|
|
688
700
|
}
|
|
689
701
|
}
|
|
690
702
|
|
|
691
|
-
//
|
|
692
|
-
|
|
703
|
+
// Auto-accept: L0 tasks or tasks with review_required: false
|
|
704
|
+
const autoAccept = isL0 || task.review_required === false;
|
|
705
|
+
if (autoAccept) {
|
|
693
706
|
const acceptError = await persist(basePath, {
|
|
694
707
|
phases: [{
|
|
695
708
|
id: phase.id,
|
|
@@ -707,7 +720,7 @@ export async function handleExecutorResult({ result, basePath = process.cwd() }
|
|
|
707
720
|
task_id: task.id,
|
|
708
721
|
review_level: reviewLevel,
|
|
709
722
|
current_review,
|
|
710
|
-
auto_accepted:
|
|
723
|
+
auto_accepted: autoAccept,
|
|
711
724
|
};
|
|
712
725
|
}
|
|
713
726
|
|
|
@@ -813,6 +826,18 @@ export async function handleDebuggerResult({ result, basePath = process.cwd() }
|
|
|
813
826
|
|
|
814
827
|
if (result.outcome === 'failed' || result.architecture_concern === true) {
|
|
815
828
|
const phaseFailed = result.architecture_concern === true;
|
|
829
|
+
|
|
830
|
+
// Determine effective workflow mode: if no tasks can make progress, escalate
|
|
831
|
+
let effectiveWorkflowMode;
|
|
832
|
+
if (phaseFailed) {
|
|
833
|
+
effectiveWorkflowMode = 'failed';
|
|
834
|
+
} else {
|
|
835
|
+
const hasProgressable = (phase.todo || []).some(t =>
|
|
836
|
+
t.id !== task.id && !['accepted', 'failed'].includes(t.lifecycle),
|
|
837
|
+
);
|
|
838
|
+
effectiveWorkflowMode = hasProgressable ? 'executing_task' : 'awaiting_user';
|
|
839
|
+
}
|
|
840
|
+
|
|
816
841
|
const phasePatch = { id: phase.id };
|
|
817
842
|
if (phaseFailed) {
|
|
818
843
|
phasePatch.lifecycle = 'failed';
|
|
@@ -820,7 +845,7 @@ export async function handleDebuggerResult({ result, basePath = process.cwd() }
|
|
|
820
845
|
phasePatch.todo = [{ id: task.id, lifecycle: 'failed', debug_context }];
|
|
821
846
|
|
|
822
847
|
const persistError = await persist(basePath, {
|
|
823
|
-
workflow_mode:
|
|
848
|
+
workflow_mode: effectiveWorkflowMode,
|
|
824
849
|
current_task: null,
|
|
825
850
|
current_review: null,
|
|
826
851
|
phases: [phasePatch],
|
|
@@ -830,7 +855,7 @@ export async function handleDebuggerResult({ result, basePath = process.cwd() }
|
|
|
830
855
|
return {
|
|
831
856
|
success: true,
|
|
832
857
|
action: phaseFailed ? 'phase_failed' : 'task_failed',
|
|
833
|
-
workflow_mode:
|
|
858
|
+
workflow_mode: effectiveWorkflowMode,
|
|
834
859
|
phase_id: phase.id,
|
|
835
860
|
task_id: task.id,
|
|
836
861
|
};
|
|
@@ -880,6 +905,7 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
880
905
|
|
|
881
906
|
const taskPatches = [];
|
|
882
907
|
let doneIncrement = 0;
|
|
908
|
+
let doneDecrement = 0;
|
|
883
909
|
|
|
884
910
|
// Accept tasks
|
|
885
911
|
for (const taskId of (result.accepted_tasks || [])) {
|
|
@@ -896,10 +922,16 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
896
922
|
const task = getTaskById(phase, taskId);
|
|
897
923
|
if (!task) continue;
|
|
898
924
|
if (task.lifecycle === 'checkpointed' || task.lifecycle === 'accepted') {
|
|
925
|
+
if (task.lifecycle === 'accepted') doneDecrement += 1;
|
|
899
926
|
taskPatches.push({ id: taskId, lifecycle: 'needs_revalidation', evidence_refs: [] });
|
|
900
927
|
}
|
|
901
928
|
}
|
|
902
929
|
|
|
930
|
+
// Snapshot accepted task IDs before propagation (for done counter adjustment)
|
|
931
|
+
const acceptedBeforePropagation = new Set(
|
|
932
|
+
(phase.todo || []).filter(t => t.lifecycle === 'accepted').map(t => t.id),
|
|
933
|
+
);
|
|
934
|
+
|
|
903
935
|
// Propagation for critical issues with invalidates_downstream
|
|
904
936
|
for (const issue of (result.critical_issues || [])) {
|
|
905
937
|
if (issue.invalidates_downstream && issue.task_id) {
|
|
@@ -911,6 +943,9 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
911
943
|
for (const task of (phase.todo || [])) {
|
|
912
944
|
if (task.lifecycle === 'needs_revalidation' && !taskPatches.some((p) => p.id === task.id)) {
|
|
913
945
|
taskPatches.push({ id: task.id, lifecycle: 'needs_revalidation', evidence_refs: [] });
|
|
946
|
+
if (acceptedBeforePropagation.has(task.id)) {
|
|
947
|
+
doneDecrement += 1;
|
|
948
|
+
}
|
|
914
949
|
}
|
|
915
950
|
}
|
|
916
951
|
|
|
@@ -919,7 +954,7 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
919
954
|
|
|
920
955
|
const phaseUpdates = {
|
|
921
956
|
id: phase.id,
|
|
922
|
-
done: (phase.done || 0) + doneIncrement,
|
|
957
|
+
done: Math.max(0, (phase.done || 0) + doneIncrement - doneDecrement),
|
|
923
958
|
phase_review: {
|
|
924
959
|
status: reviewStatus,
|
|
925
960
|
...(hasCritical ? { retry_count: (phase.phase_review?.retry_count || 0) + 1 } : {}),
|
package/src/tools/state.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import { stat } from 'node:fs/promises';
|
|
5
|
-
import { ensureDir, readJson, writeJson, writeAtomic, getStatePath, getGitHead } from '../utils.js';
|
|
5
|
+
import { ensureDir, readJson, writeJson, writeAtomic, getStatePath, getGitHead, isPlainObject } from '../utils.js';
|
|
6
6
|
import {
|
|
7
7
|
CANONICAL_FIELDS,
|
|
8
8
|
TASK_LIFECYCLE,
|
|
@@ -25,10 +25,6 @@ function withStateLock(fn) {
|
|
|
25
25
|
return p;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function isPlainObject(value) {
|
|
29
|
-
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
28
|
function inferWorkflowModeAfterResearch(state) {
|
|
33
29
|
if (state.current_review?.scope === 'phase') return 'reviewing_phase';
|
|
34
30
|
if (state.current_review?.scope === 'task') return 'reviewing_task';
|
|
@@ -73,7 +69,8 @@ export async function init({ project, phases, research, force = false, basePath
|
|
|
73
69
|
}
|
|
74
70
|
|
|
75
71
|
const state = createInitialState({ project, phases });
|
|
76
|
-
state.
|
|
72
|
+
if (state.error) return state;
|
|
73
|
+
state.git_head = await getGitHead(basePath);
|
|
77
74
|
|
|
78
75
|
// Create plan.md placeholder (atomic write)
|
|
79
76
|
await writeAtomic(
|
|
@@ -104,7 +101,7 @@ export async function init({ project, phases, research, force = false, basePath
|
|
|
104
101
|
* Read state.json, optionally filtering to specific fields.
|
|
105
102
|
*/
|
|
106
103
|
export async function read({ fields, basePath = process.cwd() } = {}) {
|
|
107
|
-
const statePath = getStatePath(basePath);
|
|
104
|
+
const statePath = await getStatePath(basePath);
|
|
108
105
|
if (!statePath) {
|
|
109
106
|
return { error: true, message: 'No .gsd directory found' };
|
|
110
107
|
}
|
|
@@ -146,7 +143,7 @@ export async function update({ updates, basePath = process.cwd() } = {}) {
|
|
|
146
143
|
};
|
|
147
144
|
}
|
|
148
145
|
|
|
149
|
-
const statePath = getStatePath(basePath);
|
|
146
|
+
const statePath = await getStatePath(basePath);
|
|
150
147
|
if (!statePath) {
|
|
151
148
|
return { error: true, message: 'No .gsd directory found' };
|
|
152
149
|
}
|
|
@@ -239,7 +236,6 @@ export async function update({ updates, basePath = process.cwd() } = {}) {
|
|
|
239
236
|
*/
|
|
240
237
|
function verificationPassed(verification) {
|
|
241
238
|
if (!verification || typeof verification !== 'object') return false;
|
|
242
|
-
if ('passed' in verification) return verification.passed === true;
|
|
243
239
|
return ['lint', 'typecheck', 'test'].every((key) => (
|
|
244
240
|
verification[key]
|
|
245
241
|
&& typeof verification[key].exit_code === 'number'
|
|
@@ -274,7 +270,7 @@ export async function phaseComplete({
|
|
|
274
270
|
if (direction_ok !== undefined && typeof direction_ok !== 'boolean') {
|
|
275
271
|
return { error: true, message: 'direction_ok must be a boolean when provided' };
|
|
276
272
|
}
|
|
277
|
-
const statePath = getStatePath(basePath);
|
|
273
|
+
const statePath = await getStatePath(basePath);
|
|
278
274
|
if (!statePath) {
|
|
279
275
|
return { error: true, message: 'No .gsd directory found' };
|
|
280
276
|
}
|
|
@@ -361,10 +357,11 @@ export async function phaseComplete({
|
|
|
361
357
|
}
|
|
362
358
|
await writeJson(statePath, state);
|
|
363
359
|
return {
|
|
364
|
-
|
|
365
|
-
|
|
360
|
+
success: true,
|
|
361
|
+
action: 'direction_drift',
|
|
366
362
|
workflow_mode: 'awaiting_user',
|
|
367
363
|
phase_id: phase.id,
|
|
364
|
+
message: 'Direction drift detected; awaiting user decision before phase can complete',
|
|
368
365
|
};
|
|
369
366
|
}
|
|
370
367
|
|
|
@@ -388,7 +385,7 @@ export async function phaseComplete({
|
|
|
388
385
|
|
|
389
386
|
// Update git_head to current commit
|
|
390
387
|
const gsdDir = dirname(statePath);
|
|
391
|
-
state.git_head = getGitHead(dirname(gsdDir));
|
|
388
|
+
state.git_head = await getGitHead(dirname(gsdDir));
|
|
392
389
|
|
|
393
390
|
// Prune evidence from old phases (in-memory to avoid double read/write)
|
|
394
391
|
await _pruneEvidenceFromState(state, state.current_phase, gsdDir);
|
|
@@ -413,7 +410,7 @@ export async function addEvidence({ id, data, basePath = process.cwd() }) {
|
|
|
413
410
|
return { error: true, message: 'data.scope must be a string' };
|
|
414
411
|
}
|
|
415
412
|
|
|
416
|
-
const statePath = getStatePath(basePath);
|
|
413
|
+
const statePath = await getStatePath(basePath);
|
|
417
414
|
if (!statePath) {
|
|
418
415
|
return { error: true, message: 'No .gsd directory found' };
|
|
419
416
|
}
|
|
@@ -475,7 +472,7 @@ async function _pruneEvidenceFromState(state, currentPhase, gsdDir) {
|
|
|
475
472
|
* Scope format is "task:X.Y" where X is the phase number.
|
|
476
473
|
*/
|
|
477
474
|
export async function pruneEvidence({ currentPhase, basePath = process.cwd() }) {
|
|
478
|
-
const statePath = getStatePath(basePath);
|
|
475
|
+
const statePath = await getStatePath(basePath);
|
|
479
476
|
if (!statePath) {
|
|
480
477
|
return { error: true, message: 'No .gsd directory found' };
|
|
481
478
|
}
|
|
@@ -524,6 +521,11 @@ export function selectRunnableTask(phase, state, { maxRetry = DEFAULT_MAX_RETRY
|
|
|
524
521
|
if (!phase || !Array.isArray(phase.todo)) {
|
|
525
522
|
return { error: true, message: 'Phase todo must be an array' };
|
|
526
523
|
}
|
|
524
|
+
// D-4: Zero-task phase — immediately trigger review so phase can advance
|
|
525
|
+
if (phase.todo.length === 0) {
|
|
526
|
+
return { mode: 'trigger_review' };
|
|
527
|
+
}
|
|
528
|
+
|
|
527
529
|
const runnableTasks = [];
|
|
528
530
|
|
|
529
531
|
for (const task of phase.todo) {
|
|
@@ -557,6 +559,12 @@ export function selectRunnableTask(phase, state, { maxRetry = DEFAULT_MAX_RETRY
|
|
|
557
559
|
return { mode: 'trigger_review' };
|
|
558
560
|
}
|
|
559
561
|
|
|
562
|
+
// All tasks accepted → trigger phase review if not already reviewed
|
|
563
|
+
const allAccepted = phase.todo.length > 0 && phase.todo.every(t => t.lifecycle === 'accepted');
|
|
564
|
+
if (allAccepted && phase.phase_review?.status !== 'accepted') {
|
|
565
|
+
return { mode: 'trigger_review' };
|
|
566
|
+
}
|
|
567
|
+
|
|
560
568
|
const blockedTasks = phase.todo.filter(t => t.lifecycle === 'blocked');
|
|
561
569
|
if (blockedTasks.length > 0) {
|
|
562
570
|
return { mode: 'awaiting_user', blockers: blockedTasks.map(t => ({ id: t.id, reason: t.blocked_reason })) };
|
|
@@ -720,7 +728,8 @@ export function reclassifyReviewLevel(task, executorResult) {
|
|
|
720
728
|
|
|
721
729
|
// Check for explicit [LEVEL-UP] in decisions
|
|
722
730
|
const hasLevelUp = (executorResult.decisions || []).some(d =>
|
|
723
|
-
typeof d === 'string' && d.includes('[LEVEL-UP]')
|
|
731
|
+
(typeof d === 'string' && d.includes('[LEVEL-UP]'))
|
|
732
|
+
|| (d && typeof d === 'object' && typeof d.summary === 'string' && d.summary.includes('[LEVEL-UP]'))
|
|
724
733
|
);
|
|
725
734
|
if (hasLevelUp) return 'L2';
|
|
726
735
|
|
|
@@ -864,7 +873,7 @@ export async function storeResearch({ result, artifacts, decision_index, basePat
|
|
|
864
873
|
return { error: true, message: `Invalid research decision_index: ${decisionIndexValidation.errors.join('; ')}` };
|
|
865
874
|
}
|
|
866
875
|
|
|
867
|
-
const statePath = getStatePath(basePath);
|
|
876
|
+
const statePath = await getStatePath(basePath);
|
|
868
877
|
if (!statePath) {
|
|
869
878
|
return { error: true, message: 'No .gsd directory found' };
|
|
870
879
|
}
|
|
@@ -898,10 +907,12 @@ export async function storeResearch({ result, artifacts, decision_index, basePat
|
|
|
898
907
|
? applyResearchRefresh(state, nextResearch)
|
|
899
908
|
: { warnings: [] };
|
|
900
909
|
|
|
910
|
+
// D-2: Compute merged decision_index explicitly before spread to avoid key-ordering fragility
|
|
911
|
+
const mergedDecisionIndex = state.research?.decision_index || decision_index;
|
|
901
912
|
state.research = {
|
|
902
913
|
...(state.research || {}),
|
|
903
914
|
...nextResearch,
|
|
904
|
-
decision_index:
|
|
915
|
+
decision_index: mergedDecisionIndex,
|
|
905
916
|
};
|
|
906
917
|
|
|
907
918
|
if (state.workflow_mode === 'research_refresh_needed') {
|
package/src/tools/verify.js
CHANGED
|
@@ -65,13 +65,16 @@ export async function runLint(pm, cwd) {
|
|
|
65
65
|
return runCommand(pm, ['run', 'lint'], cwd);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
export async function runTypeCheck(cwd) {
|
|
68
|
+
export async function runTypeCheck(pm, cwd) {
|
|
69
69
|
// M-8: Only run tsc if tsconfig.json exists
|
|
70
70
|
try {
|
|
71
71
|
await stat(join(cwd, 'tsconfig.json'));
|
|
72
72
|
} catch {
|
|
73
73
|
return { exit_code: 0, summary: 'skipped: no tsconfig.json found' };
|
|
74
74
|
}
|
|
75
|
+
if (pm === 'pnpm') return runCommand('pnpm', ['exec', 'tsc', '--noEmit'], cwd);
|
|
76
|
+
if (pm === 'yarn') return runCommand('yarn', ['tsc', '--noEmit'], cwd);
|
|
77
|
+
if (pm === 'bun') return runCommand('bun', ['run', 'tsc', '--noEmit'], cwd);
|
|
75
78
|
return runCommand('npx', ['tsc', '--noEmit'], cwd);
|
|
76
79
|
}
|
|
77
80
|
|
|
@@ -81,9 +84,10 @@ export async function runAll(cwd = process.cwd()) {
|
|
|
81
84
|
const errResult = { exit_code: -1, summary: 'No package manager detected' };
|
|
82
85
|
return { lint: errResult, typecheck: errResult, test: errResult };
|
|
83
86
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
87
|
+
const [lint, typecheck, test] = await Promise.all([
|
|
88
|
+
runLint(pm, cwd),
|
|
89
|
+
runTypeCheck(pm, cwd),
|
|
90
|
+
runTests(pm, cwd),
|
|
91
|
+
]);
|
|
92
|
+
return { lint, typecheck, test };
|
|
89
93
|
}
|