gsd-lite 0.6.3 → 0.6.5
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/.mcp.json +0 -0
- package/README.md +0 -0
- package/agents/debugger.md +0 -0
- package/agents/executor.md +0 -0
- package/agents/researcher.md +0 -0
- package/agents/reviewer.md +0 -0
- package/commands/doctor.md +0 -0
- package/commands/prd.md +0 -0
- package/commands/resume.md +0 -0
- package/commands/start.md +0 -0
- package/commands/status.md +0 -0
- package/commands/stop.md +0 -0
- package/hooks/context-monitor.js +0 -0
- package/hooks/gsd-auto-update.cjs +0 -0
- package/hooks/gsd-context-monitor.cjs +0 -0
- package/hooks/gsd-session-init.cjs +0 -0
- package/hooks/gsd-session-stop.cjs +0 -0
- package/hooks/gsd-statusline.cjs +0 -0
- package/hooks/hooks.json +2 -39
- package/hooks/lib/gsd-finder.cjs +0 -0
- package/hooks/lib/semver-sort.cjs +0 -0
- package/hooks/lib/statusline-composite.cjs +0 -0
- package/install.js +7 -27
- package/launcher.js +0 -0
- package/package.json +1 -1
- package/references/anti-rationalization-full.md +0 -0
- package/references/evidence-spec.md +0 -0
- package/references/execution-loop.md +0 -0
- package/references/git-worktrees.md +0 -0
- package/references/questioning.md +0 -0
- package/references/review-classification.md +0 -0
- package/references/state-diagram.md +0 -0
- package/references/testing-patterns.md +0 -0
- package/src/schema.js +5 -5
- package/src/server.js +38 -6
- package/src/tools/orchestrator/debugger.js +0 -0
- package/src/tools/orchestrator/executor.js +0 -0
- package/src/tools/orchestrator/helpers.js +31 -11
- package/src/tools/orchestrator/index.js +0 -0
- package/src/tools/orchestrator/researcher.js +0 -0
- package/src/tools/orchestrator/resume.js +0 -0
- package/src/tools/orchestrator/reviewer.js +0 -0
- package/src/tools/state/constants.js +0 -0
- package/src/tools/state/crud.js +38 -11
- package/src/tools/state/index.js +1 -1
- package/src/tools/state/logic.js +0 -0
- package/src/tools/verify.js +0 -0
- package/src/utils.js +0 -0
- package/uninstall.js +0 -0
- package/workflows/debugging.md +0 -0
- package/workflows/deviation-rules.md +0 -0
- package/workflows/execution-flow.md +41 -6
- package/workflows/research.md +0 -0
- package/workflows/review-cycle.md +0 -0
- package/workflows/tdd-cycle.md +0 -0
|
@@ -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.5",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
package/.mcp.json
CHANGED
|
File without changes
|
package/README.md
CHANGED
|
File without changes
|
package/agents/debugger.md
CHANGED
|
File without changes
|
package/agents/executor.md
CHANGED
|
File without changes
|
package/agents/researcher.md
CHANGED
|
File without changes
|
package/agents/reviewer.md
CHANGED
|
File without changes
|
package/commands/doctor.md
CHANGED
|
File without changes
|
package/commands/prd.md
CHANGED
|
File without changes
|
package/commands/resume.md
CHANGED
|
File without changes
|
package/commands/start.md
CHANGED
|
File without changes
|
package/commands/status.md
CHANGED
|
File without changes
|
package/commands/stop.md
CHANGED
|
File without changes
|
package/hooks/context-monitor.js
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/hooks/gsd-statusline.cjs
CHANGED
|
File without changes
|
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/hooks/lib/gsd-finder.cjs
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
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/launcher.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
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'],
|
|
@@ -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}` };
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
|
|
@@ -22,7 +23,7 @@ const RESULT_CONTRACTS = {
|
|
|
22
23
|
summary: 'string — non-empty description of work done',
|
|
23
24
|
checkpoint_commit: 'string — required when outcome="checkpointed"',
|
|
24
25
|
files_changed: 'string[] — list of modified file paths',
|
|
25
|
-
decisions: '{ id, title, rationale }[] — architectural decisions
|
|
26
|
+
decisions: '{ id, summary|title, rationale }[] — architectural decisions (summary is canonical; title accepted as alias)',
|
|
26
27
|
blockers: '{ description, type }[] — what blocked progress (when outcome="blocked")',
|
|
27
28
|
contract_changed: 'boolean — true if external API/behavior contract changed',
|
|
28
29
|
confidence: '"high" | "medium" | "low" (optional) — executor self-assessed confidence; affects review level',
|
|
@@ -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'
|
|
@@ -318,12 +336,14 @@ function buildDecisionEntries(decisions, phaseId, taskId, existingCount = 0) {
|
|
|
318
336
|
task: taskId,
|
|
319
337
|
};
|
|
320
338
|
}
|
|
321
|
-
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;
|
|
322
341
|
return {
|
|
323
342
|
id: decision.id || `decision:${phaseId}:${taskId}:${existingCount + index + 1}`,
|
|
324
343
|
phase: decision.phase ?? phaseId,
|
|
325
344
|
task: decision.task ?? taskId,
|
|
326
345
|
...decision,
|
|
346
|
+
summary,
|
|
327
347
|
};
|
|
328
348
|
}
|
|
329
349
|
return null;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
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,
|
|
@@ -23,12 +23,28 @@ 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.
|
|
29
36
|
* Missing/unreadable files are silently skipped.
|
|
30
37
|
*/
|
|
31
|
-
async function computePlanHashes(
|
|
38
|
+
export async function computePlanHashes(basePath) {
|
|
39
|
+
const gsdDir = await getGsdDir(basePath);
|
|
40
|
+
if (!gsdDir) return {};
|
|
41
|
+
const filePaths = [join(gsdDir, 'plan.md')];
|
|
42
|
+
try {
|
|
43
|
+
const phaseFiles = await readdir(join(gsdDir, 'phases'));
|
|
44
|
+
for (const f of phaseFiles) {
|
|
45
|
+
if (f.endsWith('.md')) filePaths.push(join(gsdDir, 'phases', f));
|
|
46
|
+
}
|
|
47
|
+
} catch { /* no phases dir */ }
|
|
32
48
|
const hashes = {};
|
|
33
49
|
for (const filePath of filePaths) {
|
|
34
50
|
try {
|
|
@@ -115,8 +131,9 @@ export async function init({ project, phases, research, force = false, basePath
|
|
|
115
131
|
// Date.toISOString() truncates to milliseconds. Without ceil, the stored timestamp
|
|
116
132
|
// can be slightly less than the file's actual mtime, causing false plan-drift detection.
|
|
117
133
|
state.context.last_session = new Date(Math.ceil(Math.max(...mtimes))).toISOString();
|
|
118
|
-
//
|
|
119
|
-
|
|
134
|
+
// plan_hashes left null — computed lazily on first orchestrator-resume
|
|
135
|
+
// after the agent writes actual plan.md / phases/*.md content (avoids
|
|
136
|
+
// false drift detection from hashing placeholder files).
|
|
120
137
|
await writeJson(statePath, state);
|
|
121
138
|
|
|
122
139
|
return {
|
|
@@ -144,7 +161,7 @@ export async function read({ fields, basePath = process.cwd(), validate = false
|
|
|
144
161
|
|
|
145
162
|
const result = await readJson(statePath);
|
|
146
163
|
if (!result.ok) {
|
|
147
|
-
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) };
|
|
148
165
|
}
|
|
149
166
|
const state = migrateState(result.data);
|
|
150
167
|
|
|
@@ -197,7 +214,7 @@ export async function update({ updates, basePath = process.cwd(), expectedVersio
|
|
|
197
214
|
return withStateLock(async () => {
|
|
198
215
|
const result = await readJson(statePath);
|
|
199
216
|
if (!result.ok) {
|
|
200
|
-
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) };
|
|
201
218
|
}
|
|
202
219
|
const state = migrateState(result.data);
|
|
203
220
|
|
|
@@ -325,6 +342,16 @@ export async function update({ updates, basePath = process.cwd(), expectedVersio
|
|
|
325
342
|
}
|
|
326
343
|
}
|
|
327
344
|
|
|
345
|
+
// Auto-refresh plan hashes when leaving replan_required — establishes a new
|
|
346
|
+
// baseline so the next resume doesn't re-trigger drift for already-acknowledged changes.
|
|
347
|
+
if (state.workflow_mode === 'replan_required'
|
|
348
|
+
&& updates.workflow_mode && updates.workflow_mode !== 'replan_required') {
|
|
349
|
+
const freshHashes = await computePlanHashes(basePath);
|
|
350
|
+
if (Object.keys(freshHashes).length > 0) {
|
|
351
|
+
merged.context = { ...(merged.context || {}), plan_hashes: freshHashes };
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
328
355
|
// Auto-prune evidence when entries exceed limit
|
|
329
356
|
if (merged.evidence && Object.keys(merged.evidence).length > MAX_EVIDENCE_ENTRIES) {
|
|
330
357
|
const gsdDir = dirname(statePath);
|
|
@@ -405,7 +432,7 @@ export async function phaseComplete({
|
|
|
405
432
|
return withStateLock(async () => {
|
|
406
433
|
const result = await readJson(statePath);
|
|
407
434
|
if (!result.ok) {
|
|
408
|
-
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) };
|
|
409
436
|
}
|
|
410
437
|
const state = migrateState(result.data);
|
|
411
438
|
|
|
@@ -580,7 +607,7 @@ export async function addEvidence({ id, data, basePath = process.cwd() }) {
|
|
|
580
607
|
return withStateLock(async () => {
|
|
581
608
|
const result = await readJson(statePath);
|
|
582
609
|
if (!result.ok) {
|
|
583
|
-
return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
|
|
610
|
+
return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: friendlyReadError(result.error) };
|
|
584
611
|
}
|
|
585
612
|
const state = migrateState(result.data);
|
|
586
613
|
|
|
@@ -661,7 +688,7 @@ export async function pruneEvidence({ currentPhase, basePath = process.cwd() })
|
|
|
661
688
|
return withStateLock(async () => {
|
|
662
689
|
const result = await readJson(statePath);
|
|
663
690
|
if (!result.ok) {
|
|
664
|
-
return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
|
|
691
|
+
return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: friendlyReadError(result.error) };
|
|
665
692
|
}
|
|
666
693
|
const state = migrateState(result.data);
|
|
667
694
|
|
|
@@ -701,7 +728,7 @@ export async function patchPlan({ operations, basePath = process.cwd() } = {}) {
|
|
|
701
728
|
return withStateLock(async () => {
|
|
702
729
|
const result = await readJson(statePath);
|
|
703
730
|
if (!result.ok) {
|
|
704
|
-
return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: result.error };
|
|
731
|
+
return { error: true, code: ERROR_CODES.NO_PROJECT_DIR, message: friendlyReadError(result.error) };
|
|
705
732
|
}
|
|
706
733
|
const state = migrateState(result.data);
|
|
707
734
|
|
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/src/tools/state/logic.js
CHANGED
|
File without changes
|
package/src/tools/verify.js
CHANGED
|
File without changes
|
package/src/utils.js
CHANGED
|
File without changes
|
package/uninstall.js
CHANGED
|
File without changes
|
package/workflows/debugging.md
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -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 — 最终报告
|
package/workflows/research.md
CHANGED
|
File without changes
|
|
File without changes
|
package/workflows/tdd-cycle.md
CHANGED
|
File without changes
|