gsd-lite 0.5.5 → 0.5.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hooks/gsd-auto-update.cjs +69 -0
- package/package.json +1 -1
- package/references/review-classification.md +4 -3
- package/src/schema.js +3 -2
- package/src/server.js +28 -2
- package/src/tools/orchestrator.js +106 -7
- package/src/tools/state.js +37 -10
|
@@ -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.5.
|
|
16
|
+
"version": "0.5.7",
|
|
17
17
|
"keywords": [
|
|
18
18
|
"orchestration",
|
|
19
19
|
"mcp",
|
|
@@ -393,6 +393,11 @@ async function downloadAndInstall(tarballUrl, verbose = false, token = null) {
|
|
|
393
393
|
try { fs.rmSync(backupPath, { force: true }); } catch { /* ignore */ }
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
// Sync to plugin cache if installed as plugin
|
|
397
|
+
// The MCP server loads from plugins/cache, not the runtime dir,
|
|
398
|
+
// so we must update the cache for version changes to take effect.
|
|
399
|
+
syncPluginCache(tmpDir, verbose);
|
|
400
|
+
|
|
396
401
|
return true;
|
|
397
402
|
} catch (err) {
|
|
398
403
|
if (verbose) console.error(' Install failed:', err.message);
|
|
@@ -437,6 +442,70 @@ function writeNotification(notification) {
|
|
|
437
442
|
}
|
|
438
443
|
}
|
|
439
444
|
|
|
445
|
+
// ── Plugin Cache Sync ─────────────────────────────────────
|
|
446
|
+
// When installed as a plugin, the MCP server runs from plugins/cache/gsd/gsd/<version>/
|
|
447
|
+
// The auto-update installs to ~/.claude/gsd/ (runtime dir) via install.js,
|
|
448
|
+
// but must ALSO update the cache directory for the MCP server to pick up changes.
|
|
449
|
+
function syncPluginCache(extractedDir, verbose = false) {
|
|
450
|
+
try {
|
|
451
|
+
const pluginsFile = path.join(claudeDir, 'plugins', 'installed_plugins.json');
|
|
452
|
+
if (!fs.existsSync(pluginsFile)) return;
|
|
453
|
+
|
|
454
|
+
const plugins = JSON.parse(fs.readFileSync(pluginsFile, 'utf8'));
|
|
455
|
+
const gsdEntry = plugins.plugins?.['gsd@gsd']?.[0];
|
|
456
|
+
if (!gsdEntry?.installPath) return;
|
|
457
|
+
|
|
458
|
+
// Read new version from extracted package
|
|
459
|
+
const newPkgPath = path.join(extractedDir, 'package.json');
|
|
460
|
+
if (!fs.existsSync(newPkgPath)) return;
|
|
461
|
+
const newVersion = JSON.parse(fs.readFileSync(newPkgPath, 'utf8')).version;
|
|
462
|
+
if (!newVersion) return;
|
|
463
|
+
|
|
464
|
+
// Determine new cache path
|
|
465
|
+
const cacheBase = path.join(claudeDir, 'plugins', 'cache', 'gsd', 'gsd');
|
|
466
|
+
const newCachePath = path.join(cacheBase, newVersion);
|
|
467
|
+
|
|
468
|
+
// Skip if already up to date
|
|
469
|
+
if (gsdEntry.installPath === newCachePath && fs.existsSync(newCachePath)) {
|
|
470
|
+
const existingVersion = (() => {
|
|
471
|
+
try { return JSON.parse(fs.readFileSync(path.join(newCachePath, 'package.json'), 'utf8')).version; }
|
|
472
|
+
catch { return null; }
|
|
473
|
+
})();
|
|
474
|
+
if (existingVersion === newVersion) {
|
|
475
|
+
if (verbose) console.log(' Plugin cache already up to date');
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Copy extracted files to new cache directory
|
|
481
|
+
if (verbose) console.log(` Syncing plugin cache → ${newCachePath}`);
|
|
482
|
+
fs.mkdirSync(newCachePath, { recursive: true });
|
|
483
|
+
fs.cpSync(extractedDir, newCachePath, { recursive: true });
|
|
484
|
+
|
|
485
|
+
// Install dependencies in cache dir
|
|
486
|
+
if (!fs.existsSync(path.join(newCachePath, 'node_modules', '@modelcontextprotocol'))) {
|
|
487
|
+
spawnSync('npm', ['install', '--omit=dev', '--ignore-scripts'], {
|
|
488
|
+
cwd: newCachePath,
|
|
489
|
+
stdio: 'pipe',
|
|
490
|
+
timeout: 60000,
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Update installed_plugins.json to point to new cache path
|
|
495
|
+
gsdEntry.installPath = newCachePath;
|
|
496
|
+
gsdEntry.version = newVersion;
|
|
497
|
+
gsdEntry.lastUpdated = new Date().toISOString();
|
|
498
|
+
const tmpPlugins = pluginsFile + `.${process.pid}.tmp`;
|
|
499
|
+
fs.writeFileSync(tmpPlugins, JSON.stringify(plugins, null, 2) + '\n');
|
|
500
|
+
fs.renameSync(tmpPlugins, pluginsFile);
|
|
501
|
+
|
|
502
|
+
if (verbose) console.log(` Plugin cache synced to v${newVersion}`);
|
|
503
|
+
} catch (err) {
|
|
504
|
+
// Best effort — don't fail the update if cache sync fails
|
|
505
|
+
if (verbose) console.error(' Plugin cache sync failed:', err.message);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
440
509
|
module.exports = {
|
|
441
510
|
checkForUpdate,
|
|
442
511
|
getCurrentVersion,
|
package/package.json
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
| L0 | 无运行时语义变化 (docs/config/style) | checkpoint 后直接 accepted |
|
|
8
8
|
| L1 | 普通编码任务 (默认) | phase 结束后批量审查 |
|
|
9
9
|
| L2 | 高风险 (auth/payment/public API/DB migration) | checkpoint 后立即独立审查 |
|
|
10
|
+
| L3 | 最高风险 (复杂架构/关键系统) | 与 L2 相同: checkpoint 后立即独立审查 |
|
|
10
11
|
|
|
11
12
|
## 运行时重分类
|
|
12
13
|
|
|
@@ -62,14 +63,14 @@ executor checkpointed
|
|
|
62
63
|
-> 批量审查所有 checkpointed task (排除 L0)
|
|
63
64
|
```
|
|
64
65
|
|
|
65
|
-
### L2 流程
|
|
66
|
+
### L2/L3 流程
|
|
66
67
|
|
|
67
68
|
```
|
|
68
69
|
executor checkpointed
|
|
69
|
-
-> handleExecutorResult 检测 reviewLevel === 'L2' && review_required !== false
|
|
70
|
+
-> handleExecutorResult 检测 (reviewLevel === 'L2' || reviewLevel === 'L3') && review_required !== false
|
|
70
71
|
-> 设置 current_review = { scope: 'task', scope_id: task.id, stage: 'spec' }
|
|
71
72
|
-> workflow_mode = 'reviewing_task'
|
|
72
|
-
-> 派发 reviewer (scope='task', review_level='L2')
|
|
73
|
+
-> 派发 reviewer (scope='task', review_level='L2' 或 'L3')
|
|
73
74
|
-> 审查通过后才释放下游依赖
|
|
74
75
|
```
|
|
75
76
|
|
package/src/schema.js
CHANGED
|
@@ -619,8 +619,9 @@ export function validateReviewerResult(r) {
|
|
|
619
619
|
errors.push('critical_issues entries must be objects');
|
|
620
620
|
continue;
|
|
621
621
|
}
|
|
622
|
-
|
|
623
|
-
|
|
622
|
+
const issueText = issue.reason ?? issue.description;
|
|
623
|
+
if (typeof issueText !== 'string' || issueText.length === 0) {
|
|
624
|
+
errors.push('critical_issues[].reason (or .description) must be non-empty string');
|
|
624
625
|
}
|
|
625
626
|
if ('task_id' in issue && typeof issue.task_id !== 'string') {
|
|
626
627
|
errors.push('critical_issues[].task_id must be string');
|
package/src/server.js
CHANGED
|
@@ -7,6 +7,24 @@ import { init, read, update, phaseComplete } from './tools/state.js';
|
|
|
7
7
|
|
|
8
8
|
const _require = createRequire(import.meta.url);
|
|
9
9
|
const PKG_VERSION = _require('../package.json').version;
|
|
10
|
+
|
|
11
|
+
// Dev-mode version drift detection: warn when running code differs from disk
|
|
12
|
+
const _isDevMode = (() => {
|
|
13
|
+
try { return _require('node:fs').existsSync(new URL('../.git', import.meta.url)); } catch { return false; }
|
|
14
|
+
})();
|
|
15
|
+
function _checkVersionDrift() {
|
|
16
|
+
if (!_isDevMode) return null;
|
|
17
|
+
try {
|
|
18
|
+
// Clear require cache to read fresh package.json
|
|
19
|
+
const pkgPath = _require.resolve('../package.json');
|
|
20
|
+
delete _require.cache[pkgPath];
|
|
21
|
+
const diskVersion = _require('../package.json').version;
|
|
22
|
+
if (diskVersion !== PKG_VERSION) {
|
|
23
|
+
return `⚠️ GSD server running v${PKG_VERSION} but code on disk is v${diskVersion}. Run /mcp to restart.`;
|
|
24
|
+
}
|
|
25
|
+
} catch { /* ignore */ }
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
10
28
|
import {
|
|
11
29
|
handleDebuggerResult,
|
|
12
30
|
handleExecutorResult,
|
|
@@ -130,7 +148,13 @@ const TOOLS = [
|
|
|
130
148
|
description: 'Resume the minimal orchestration loop from workflow_mode/current_phase state',
|
|
131
149
|
inputSchema: {
|
|
132
150
|
type: 'object',
|
|
133
|
-
properties: {
|
|
151
|
+
properties: {
|
|
152
|
+
unblock_tasks: {
|
|
153
|
+
type: 'array',
|
|
154
|
+
items: { type: 'string' },
|
|
155
|
+
description: 'Optional task IDs to force-unblock before resuming (e.g. ["1.1", "2.3"])',
|
|
156
|
+
},
|
|
157
|
+
},
|
|
134
158
|
},
|
|
135
159
|
},
|
|
136
160
|
{
|
|
@@ -185,7 +209,7 @@ const TOOLS = [
|
|
|
185
209
|
properties: {
|
|
186
210
|
result: {
|
|
187
211
|
type: 'object',
|
|
188
|
-
description: 'Reviewer result: {scope: "task"|"phase", scope_id: string|number, review_level: "L2"|"L1-batch", spec_passed: boolean, quality_passed: boolean, critical_issues:
|
|
212
|
+
description: 'Reviewer result: {scope: "task"|"phase", scope_id: string|number, review_level: "L2"|"L1-batch", spec_passed: boolean, quality_passed: boolean, critical_issues: [{reason|description: string, task_id?: string, invalidates_downstream?: boolean}], important_issues: [{reason|description: string}], minor_issues: object[], accepted_tasks: string[], rework_tasks: string[], evidence: object[]}',
|
|
189
213
|
},
|
|
190
214
|
},
|
|
191
215
|
required: ['result'],
|
|
@@ -202,6 +226,7 @@ async function dispatchToolCall(name, args) {
|
|
|
202
226
|
switch (name) {
|
|
203
227
|
case 'health': {
|
|
204
228
|
const stateResult = await read(args || {});
|
|
229
|
+
const versionDrift = _checkVersionDrift();
|
|
205
230
|
result = {
|
|
206
231
|
status: 'ok',
|
|
207
232
|
server: 'gsd',
|
|
@@ -213,6 +238,7 @@ async function dispatchToolCall(name, args) {
|
|
|
213
238
|
current_phase: stateResult.current_phase,
|
|
214
239
|
total_phases: stateResult.total_phases,
|
|
215
240
|
}),
|
|
241
|
+
...(versionDrift ? { warning: versionDrift } : {}),
|
|
216
242
|
};
|
|
217
243
|
break;
|
|
218
244
|
}
|
|
@@ -18,6 +18,56 @@ const MAX_RESUME_DEPTH = 3;
|
|
|
18
18
|
const CONTEXT_RESUME_THRESHOLD = 40;
|
|
19
19
|
const MAX_DECISIONS = 200;
|
|
20
20
|
|
|
21
|
+
// ── Result Contracts ──
|
|
22
|
+
// Provided in dispatch responses so agents produce valid results on the first call.
|
|
23
|
+
const RESULT_CONTRACTS = {
|
|
24
|
+
executor: {
|
|
25
|
+
task_id: 'string — must match dispatched task_id',
|
|
26
|
+
outcome: '"checkpointed" | "blocked" | "failed"',
|
|
27
|
+
summary: 'string — non-empty description of work done',
|
|
28
|
+
checkpoint_commit: 'string — required when outcome="checkpointed"',
|
|
29
|
+
files_changed: 'string[] — list of modified file paths',
|
|
30
|
+
decisions: '{ id, title, rationale }[] — architectural decisions made',
|
|
31
|
+
blockers: '{ description, type }[] — what blocked progress (when outcome="blocked")',
|
|
32
|
+
contract_changed: 'boolean — true if external API/behavior contract changed',
|
|
33
|
+
evidence: '{ type, detail }[] — verification evidence (test results, lint, etc.)',
|
|
34
|
+
},
|
|
35
|
+
reviewer: {
|
|
36
|
+
scope: '"task" | "phase"',
|
|
37
|
+
scope_id: 'string | number — task id (e.g. "1.2") or phase number',
|
|
38
|
+
review_level: '"L2" | "L1-batch" | "L1"',
|
|
39
|
+
spec_passed: 'boolean',
|
|
40
|
+
quality_passed: 'boolean',
|
|
41
|
+
critical_issues: '{ reason|description, task_id?, invalidates_downstream? }[] — blocking issues',
|
|
42
|
+
important_issues: '{ description, task_id? }[]',
|
|
43
|
+
minor_issues: '{ description, task_id? }[]',
|
|
44
|
+
accepted_tasks: 'string[] — task ids that passed review',
|
|
45
|
+
rework_tasks: 'string[] — task ids that need rework (disjoint with accepted_tasks)',
|
|
46
|
+
evidence: '{ type, detail }[]',
|
|
47
|
+
},
|
|
48
|
+
researcher: {
|
|
49
|
+
result: {
|
|
50
|
+
decision_ids: 'string[] — ids of decisions addressed',
|
|
51
|
+
volatility: '"low" | "medium" | "high"',
|
|
52
|
+
expires_at: 'string — ISO date when research expires',
|
|
53
|
+
sources: '{ id, type, ref, title?, accessed_at? }[] — research sources',
|
|
54
|
+
},
|
|
55
|
+
decision_index: '{ [id]: { id, title, rationale, status, summary } } — keyed by decision id',
|
|
56
|
+
artifacts: '{ "STACK.md", "ARCHITECTURE.md", "PITFALLS.md", "SUMMARY.md" } — all four required',
|
|
57
|
+
},
|
|
58
|
+
debugger: {
|
|
59
|
+
task_id: 'string — must match debug target',
|
|
60
|
+
outcome: '"root_cause_found" | "fix_suggested" | "failed"',
|
|
61
|
+
root_cause: 'string — non-empty root cause description',
|
|
62
|
+
evidence: '{ type, detail }[]',
|
|
63
|
+
hypothesis_tested: '{ hypothesis, result: "confirmed"|"rejected", evidence }[]',
|
|
64
|
+
fix_direction: 'string — recommended fix approach',
|
|
65
|
+
fix_attempts: 'number — non-negative integer (>=3 requires outcome="failed")',
|
|
66
|
+
blockers: '{ description, type }[]',
|
|
67
|
+
architecture_concern: 'boolean',
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
21
71
|
function isTerminalWorkflowMode(workflowMode) {
|
|
22
72
|
return workflowMode === 'completed' || workflowMode === 'failed';
|
|
23
73
|
}
|
|
@@ -353,6 +403,7 @@ function buildExecutorDispatch(state, phase, task, extras = {}) {
|
|
|
353
403
|
phase_id: phase.id,
|
|
354
404
|
task_id: task.id,
|
|
355
405
|
executor_context: context,
|
|
406
|
+
result_contract: RESULT_CONTRACTS.executor,
|
|
356
407
|
...extras,
|
|
357
408
|
};
|
|
358
409
|
}
|
|
@@ -420,6 +471,7 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
420
471
|
phase_id: phase.id,
|
|
421
472
|
current_review: state.current_review,
|
|
422
473
|
debug_target: getDebugTarget(phase, task, state.current_review),
|
|
474
|
+
result_contract: RESULT_CONTRACTS.debugger,
|
|
423
475
|
};
|
|
424
476
|
}
|
|
425
477
|
|
|
@@ -450,8 +502,9 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
450
502
|
|
|
451
503
|
if (selection.task) {
|
|
452
504
|
const task = selection.task;
|
|
453
|
-
//
|
|
454
|
-
|
|
505
|
+
// Compound transition: auto-reset to pending for states that require it
|
|
506
|
+
// needs_revalidation/blocked/failed all transition through pending before running
|
|
507
|
+
if (['needs_revalidation', 'blocked', 'failed'].includes(task.lifecycle)) {
|
|
455
508
|
const resetError = await persist(basePath, {
|
|
456
509
|
phases: [{ id: phase.id, todo: [{ id: task.id, lifecycle: 'pending' }] }],
|
|
457
510
|
});
|
|
@@ -520,6 +573,14 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
520
573
|
const reviewPassed = phase.phase_review?.status === 'accepted'
|
|
521
574
|
|| phase.phase_handoff?.required_reviews_passed === true;
|
|
522
575
|
if (allAccepted && reviewPassed) {
|
|
576
|
+
// Auto-advance phase lifecycle to 'reviewing' if currently 'active'
|
|
577
|
+
// (mirrors trigger_review path at line 480-482)
|
|
578
|
+
if (phase.lifecycle === 'active') {
|
|
579
|
+
const advanceError = await persist(basePath, {
|
|
580
|
+
phases: [{ id: phase.id, lifecycle: 'reviewing' }],
|
|
581
|
+
});
|
|
582
|
+
if (advanceError) return advanceError;
|
|
583
|
+
}
|
|
523
584
|
return {
|
|
524
585
|
success: true,
|
|
525
586
|
action: 'complete_phase',
|
|
@@ -544,7 +605,7 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
544
605
|
};
|
|
545
606
|
}
|
|
546
607
|
|
|
547
|
-
export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0 } = {}) {
|
|
608
|
+
export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0, unblock_tasks } = {}) {
|
|
548
609
|
if (_depth >= MAX_RESUME_DEPTH) {
|
|
549
610
|
return { error: true, message: `resumeWorkflow recursive depth limit exceeded (max ${MAX_RESUME_DEPTH})` };
|
|
550
611
|
}
|
|
@@ -554,6 +615,31 @@ export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0 } =
|
|
|
554
615
|
return state;
|
|
555
616
|
}
|
|
556
617
|
|
|
618
|
+
// Force-unblock specified tasks before normal resume flow
|
|
619
|
+
if (Array.isArray(unblock_tasks) && unblock_tasks.length > 0 && _depth === 0) {
|
|
620
|
+
const phase = getCurrentPhase(state);
|
|
621
|
+
if (phase) {
|
|
622
|
+
const patches = [];
|
|
623
|
+
for (const taskId of unblock_tasks) {
|
|
624
|
+
const task = (phase.todo || []).find(t => t.id === taskId);
|
|
625
|
+
if (task?.lifecycle === 'blocked') {
|
|
626
|
+
patches.push({ id: taskId, lifecycle: 'pending', blocked_reason: null, unblock_condition: null });
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
if (patches.length > 0) {
|
|
630
|
+
const persistError = await persist(basePath, {
|
|
631
|
+
workflow_mode: 'executing_task',
|
|
632
|
+
current_task: null,
|
|
633
|
+
current_review: null,
|
|
634
|
+
phases: [{ id: phase.id, todo: patches }],
|
|
635
|
+
});
|
|
636
|
+
if (persistError) return persistError;
|
|
637
|
+
// Re-read state after unblock and continue
|
|
638
|
+
return resumeWorkflow({ basePath, _depth: _depth + 1 });
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
557
643
|
const preflight = await evaluatePreflight(state, basePath);
|
|
558
644
|
if (preflight.override) {
|
|
559
645
|
const persistError = await persist(basePath, preflight.override.updates);
|
|
@@ -600,7 +686,7 @@ export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0 } =
|
|
|
600
686
|
const autoUnblock = await tryAutoUnblock(state, phase, basePath);
|
|
601
687
|
if (autoUnblock.error) return autoUnblock;
|
|
602
688
|
|
|
603
|
-
if (autoUnblock.
|
|
689
|
+
if (autoUnblock.blockers.length === 0) {
|
|
604
690
|
const persistError = await persist(basePath, {
|
|
605
691
|
workflow_mode: 'executing_task',
|
|
606
692
|
current_task: null,
|
|
@@ -643,6 +729,7 @@ export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0 } =
|
|
|
643
729
|
checkpoint_commit: task.checkpoint_commit || null,
|
|
644
730
|
files_changed: task.files_changed || [],
|
|
645
731
|
})),
|
|
732
|
+
result_contract: RESULT_CONTRACTS.reviewer,
|
|
646
733
|
};
|
|
647
734
|
}
|
|
648
735
|
case 'reviewing_task': {
|
|
@@ -670,6 +757,7 @@ export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0 } =
|
|
|
670
757
|
checkpoint_commit: task.checkpoint_commit || null,
|
|
671
758
|
files_changed: task.files_changed || [],
|
|
672
759
|
} : null,
|
|
760
|
+
result_contract: RESULT_CONTRACTS.reviewer,
|
|
673
761
|
};
|
|
674
762
|
}
|
|
675
763
|
case 'completed':
|
|
@@ -796,7 +884,7 @@ export async function handleExecutorResult({ result, basePath = process.cwd() }
|
|
|
796
884
|
const isL0 = reviewLevel === 'L0';
|
|
797
885
|
const autoAccept = isL0 || task.review_required === false;
|
|
798
886
|
|
|
799
|
-
const current_review = !isL0 && reviewLevel === 'L2' && task.review_required !== false
|
|
887
|
+
const current_review = !isL0 && (reviewLevel === 'L2' || reviewLevel === 'L3') && task.review_required !== false
|
|
800
888
|
? { scope: 'task', scope_id: task.id, stage: 'spec' }
|
|
801
889
|
: null;
|
|
802
890
|
const workflow_mode = current_review ? 'reviewing_task' : 'executing_task';
|
|
@@ -843,6 +931,7 @@ export async function handleExecutorResult({ result, basePath = process.cwd() }
|
|
|
843
931
|
review_level: reviewLevel,
|
|
844
932
|
current_review,
|
|
845
933
|
auto_accepted: autoAccept,
|
|
934
|
+
...(current_review ? { result_contract: RESULT_CONTRACTS.reviewer } : {}),
|
|
846
935
|
};
|
|
847
936
|
}
|
|
848
937
|
|
|
@@ -920,6 +1009,7 @@ export async function handleExecutorResult({ result, basePath = process.cwd() }
|
|
|
920
1009
|
task_id: task.id,
|
|
921
1010
|
retry_count,
|
|
922
1011
|
current_review,
|
|
1012
|
+
result_contract: shouldDebug ? RESULT_CONTRACTS.debugger : RESULT_CONTRACTS.executor,
|
|
923
1013
|
};
|
|
924
1014
|
}
|
|
925
1015
|
|
|
@@ -1039,12 +1129,21 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
1039
1129
|
}
|
|
1040
1130
|
}
|
|
1041
1131
|
|
|
1042
|
-
// Rework tasks
|
|
1132
|
+
// Rework tasks — persist reviewer feedback so executor knows what to fix
|
|
1043
1133
|
for (const taskId of (result.rework_tasks || [])) {
|
|
1044
1134
|
const task = getTaskById(phase, taskId);
|
|
1045
1135
|
if (!task) continue;
|
|
1046
1136
|
if (task.lifecycle === 'checkpointed' || task.lifecycle === 'accepted') {
|
|
1047
|
-
|
|
1137
|
+
const taskIssues = [
|
|
1138
|
+
...(result.critical_issues || []).filter(i => !i.task_id || i.task_id === taskId),
|
|
1139
|
+
...(result.important_issues || []).filter(i => !i.task_id || i.task_id === taskId),
|
|
1140
|
+
].map(i => i.reason ?? i.description ?? '');
|
|
1141
|
+
taskPatches.push({
|
|
1142
|
+
id: taskId,
|
|
1143
|
+
lifecycle: 'needs_revalidation',
|
|
1144
|
+
evidence_refs: [],
|
|
1145
|
+
last_review_feedback: taskIssues.length > 0 ? taskIssues : null,
|
|
1146
|
+
});
|
|
1048
1147
|
}
|
|
1049
1148
|
}
|
|
1050
1149
|
|
package/src/tools/state.js
CHANGED
|
@@ -146,7 +146,17 @@ export async function init({ project, phases, research, force = false, basePath
|
|
|
146
146
|
state.context.last_session = new Date(Math.ceil(Math.max(...mtimes))).toISOString();
|
|
147
147
|
await writeJson(statePath, state);
|
|
148
148
|
|
|
149
|
-
return {
|
|
149
|
+
return {
|
|
150
|
+
success: true,
|
|
151
|
+
project: state.project,
|
|
152
|
+
total_phases: state.total_phases,
|
|
153
|
+
phases: state.phases.map(p => ({
|
|
154
|
+
id: p.id,
|
|
155
|
+
name: p.name,
|
|
156
|
+
tasks: p.todo.length,
|
|
157
|
+
})),
|
|
158
|
+
research: !!research,
|
|
159
|
+
};
|
|
150
160
|
});
|
|
151
161
|
}
|
|
152
162
|
|
|
@@ -394,13 +404,23 @@ export async function phaseComplete({
|
|
|
394
404
|
}
|
|
395
405
|
|
|
396
406
|
// Validate phase lifecycle transition FIRST (fail-fast) [I-4]
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
phase
|
|
400
|
-
'accepted'
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
407
|
+
// Allow active → accepted by auto-advancing through 'reviewing' intermediate state
|
|
408
|
+
if (phase.lifecycle === 'active') {
|
|
409
|
+
const intermediateResult = validateTransition('phase', 'active', 'reviewing');
|
|
410
|
+
const finalResult = validateTransition('phase', 'reviewing', 'accepted');
|
|
411
|
+
if (!intermediateResult.valid || !finalResult.valid) {
|
|
412
|
+
return { error: true, code: ERROR_CODES.TRANSITION_ERROR, message: `Invalid phase transition: ${phase.lifecycle} → accepted` };
|
|
413
|
+
}
|
|
414
|
+
// Will be set to 'accepted' below; just validate here
|
|
415
|
+
} else {
|
|
416
|
+
const transitionResult = validateTransition(
|
|
417
|
+
'phase',
|
|
418
|
+
phase.lifecycle,
|
|
419
|
+
'accepted',
|
|
420
|
+
);
|
|
421
|
+
if (!transitionResult.valid) {
|
|
422
|
+
return { error: true, code: ERROR_CODES.TRANSITION_ERROR, message: transitionResult.error };
|
|
423
|
+
}
|
|
404
424
|
}
|
|
405
425
|
|
|
406
426
|
// Check handoff gate: all tasks must be accepted
|
|
@@ -670,7 +690,8 @@ export function selectRunnableTask(phase, state, { maxRetry = DEFAULT_MAX_RETRY
|
|
|
670
690
|
if (gate === 'accepted' && depTask.lifecycle !== 'accepted') { depsOk = false; break; }
|
|
671
691
|
if (gate === 'phase_complete') { depsOk = false; break; } // phase_complete is only valid on phase-kind deps
|
|
672
692
|
} else if (dep.kind === 'phase') {
|
|
673
|
-
const
|
|
693
|
+
const depPhaseId = Number(dep.id);
|
|
694
|
+
const depPhase = (state.phases || []).find(p => p.id === depPhaseId);
|
|
674
695
|
if (!depPhase || depPhase.lifecycle !== 'accepted') { depsOk = false; break; }
|
|
675
696
|
}
|
|
676
697
|
}
|
|
@@ -725,7 +746,8 @@ export function selectRunnableTask(phase, state, { maxRetry = DEFAULT_MAX_RETRY
|
|
|
725
746
|
reasons.push(`dep ${dep.id} has phase_complete gate (invalid for task-kind dependency)`);
|
|
726
747
|
}
|
|
727
748
|
} else if (dep.kind === 'phase') {
|
|
728
|
-
const
|
|
749
|
+
const depPhaseId = Number(dep.id);
|
|
750
|
+
const depPhase = (state.phases || []).find(p => p.id === depPhaseId);
|
|
729
751
|
if (!depPhase || depPhase.lifecycle !== 'accepted') {
|
|
730
752
|
reasons.push(`phase dep ${dep.id} not accepted`);
|
|
731
753
|
}
|
|
@@ -827,6 +849,10 @@ export function buildExecutorContext(state, taskId, phaseId) {
|
|
|
827
849
|
evidence: task.debug_context.evidence || [],
|
|
828
850
|
} : null;
|
|
829
851
|
|
|
852
|
+
const rework_feedback = Array.isArray(task.last_review_feedback) && task.last_review_feedback.length > 0
|
|
853
|
+
? task.last_review_feedback
|
|
854
|
+
: null;
|
|
855
|
+
|
|
830
856
|
return {
|
|
831
857
|
task_spec,
|
|
832
858
|
research_decisions,
|
|
@@ -835,6 +861,7 @@ export function buildExecutorContext(state, taskId, phaseId) {
|
|
|
835
861
|
workflows,
|
|
836
862
|
constraints,
|
|
837
863
|
debugger_guidance,
|
|
864
|
+
rework_feedback,
|
|
838
865
|
};
|
|
839
866
|
}
|
|
840
867
|
|