gsd-lite 0.5.4 → 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/agents/debugger.md +4 -2
- package/agents/reviewer.md +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 +4 -3
- package/src/server.js +28 -2
- package/src/tools/orchestrator.js +111 -5
- 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",
|
package/agents/debugger.md
CHANGED
|
@@ -67,9 +67,11 @@ Phase 4 修复方向建议:
|
|
|
67
67
|
"task_id": "2.3",
|
|
68
68
|
"outcome": "root_cause_found | fix_suggested | failed",
|
|
69
69
|
"root_cause": "Description of the identified root cause",
|
|
70
|
-
"evidence": [
|
|
70
|
+
"evidence": [
|
|
71
|
+
{ "id": "ev:repro:error-xyz", "scope": "task:2.3", "command": "npm test", "exit_code": 1, "stdout": "...", "stderr": "...", "timestamp": "ISO8601" }
|
|
72
|
+
],
|
|
71
73
|
"hypothesis_tested": [
|
|
72
|
-
{ "hypothesis": "X causes Y", "result": "confirmed | rejected", "evidence": "
|
|
74
|
+
{ "hypothesis": "X causes Y", "result": "confirmed | rejected", "evidence": "non-empty string (required)" }
|
|
73
75
|
],
|
|
74
76
|
"fix_direction": "Suggested fix approach for executor",
|
|
75
77
|
"fix_attempts": 0,
|
package/agents/reviewer.md
CHANGED
|
@@ -93,7 +93,7 @@ Minor = 建议修复 (命名/风格)
|
|
|
93
93
|
{
|
|
94
94
|
"scope": "task | phase",
|
|
95
95
|
"scope_id": "2.3 (task scope: string ID) | 2 (phase scope: number ID)",
|
|
96
|
-
"review_level": "L2 | L1-batch",
|
|
96
|
+
"review_level": "L2 | L1-batch | L1",
|
|
97
97
|
"spec_passed": true,
|
|
98
98
|
"quality_passed": false,
|
|
99
99
|
"critical_issues": [
|
|
@@ -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
|
@@ -598,7 +598,7 @@ export function validateReviewerResult(r) {
|
|
|
598
598
|
if (!(typeof r.scope_id === 'string' || typeof r.scope_id === 'number') || r.scope_id === '') {
|
|
599
599
|
errors.push('missing scope_id');
|
|
600
600
|
}
|
|
601
|
-
if (!['L2', 'L1-batch'].includes(r.review_level)) errors.push('invalid review_level');
|
|
601
|
+
if (!['L2', 'L1-batch', 'L1'].includes(r.review_level)) errors.push('invalid review_level (expected L2, L1-batch, or L1)');
|
|
602
602
|
if (typeof r.spec_passed !== 'boolean') errors.push('spec_passed must be boolean');
|
|
603
603
|
if (typeof r.quality_passed !== 'boolean') errors.push('quality_passed must be boolean');
|
|
604
604
|
if (!Array.isArray(r.critical_issues)) errors.push('critical_issues must be array');
|
|
@@ -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,6 +502,14 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
450
502
|
|
|
451
503
|
if (selection.task) {
|
|
452
504
|
const task = selection.task;
|
|
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)) {
|
|
508
|
+
const resetError = await persist(basePath, {
|
|
509
|
+
phases: [{ id: phase.id, todo: [{ id: task.id, lifecycle: 'pending' }] }],
|
|
510
|
+
});
|
|
511
|
+
if (resetError) return resetError;
|
|
512
|
+
}
|
|
453
513
|
const persistError = await persist(basePath, {
|
|
454
514
|
workflow_mode: 'executing_task',
|
|
455
515
|
current_task: task.id,
|
|
@@ -513,6 +573,14 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
513
573
|
const reviewPassed = phase.phase_review?.status === 'accepted'
|
|
514
574
|
|| phase.phase_handoff?.required_reviews_passed === true;
|
|
515
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
|
+
}
|
|
516
584
|
return {
|
|
517
585
|
success: true,
|
|
518
586
|
action: 'complete_phase',
|
|
@@ -537,7 +605,7 @@ async function resumeExecutingTask(state, basePath) {
|
|
|
537
605
|
};
|
|
538
606
|
}
|
|
539
607
|
|
|
540
|
-
export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0 } = {}) {
|
|
608
|
+
export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0, unblock_tasks } = {}) {
|
|
541
609
|
if (_depth >= MAX_RESUME_DEPTH) {
|
|
542
610
|
return { error: true, message: `resumeWorkflow recursive depth limit exceeded (max ${MAX_RESUME_DEPTH})` };
|
|
543
611
|
}
|
|
@@ -547,6 +615,31 @@ export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0 } =
|
|
|
547
615
|
return state;
|
|
548
616
|
}
|
|
549
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
|
+
|
|
550
643
|
const preflight = await evaluatePreflight(state, basePath);
|
|
551
644
|
if (preflight.override) {
|
|
552
645
|
const persistError = await persist(basePath, preflight.override.updates);
|
|
@@ -593,7 +686,7 @@ export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0 } =
|
|
|
593
686
|
const autoUnblock = await tryAutoUnblock(state, phase, basePath);
|
|
594
687
|
if (autoUnblock.error) return autoUnblock;
|
|
595
688
|
|
|
596
|
-
if (autoUnblock.
|
|
689
|
+
if (autoUnblock.blockers.length === 0) {
|
|
597
690
|
const persistError = await persist(basePath, {
|
|
598
691
|
workflow_mode: 'executing_task',
|
|
599
692
|
current_task: null,
|
|
@@ -636,6 +729,7 @@ export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0 } =
|
|
|
636
729
|
checkpoint_commit: task.checkpoint_commit || null,
|
|
637
730
|
files_changed: task.files_changed || [],
|
|
638
731
|
})),
|
|
732
|
+
result_contract: RESULT_CONTRACTS.reviewer,
|
|
639
733
|
};
|
|
640
734
|
}
|
|
641
735
|
case 'reviewing_task': {
|
|
@@ -663,6 +757,7 @@ export async function resumeWorkflow({ basePath = process.cwd(), _depth = 0 } =
|
|
|
663
757
|
checkpoint_commit: task.checkpoint_commit || null,
|
|
664
758
|
files_changed: task.files_changed || [],
|
|
665
759
|
} : null,
|
|
760
|
+
result_contract: RESULT_CONTRACTS.reviewer,
|
|
666
761
|
};
|
|
667
762
|
}
|
|
668
763
|
case 'completed':
|
|
@@ -789,7 +884,7 @@ export async function handleExecutorResult({ result, basePath = process.cwd() }
|
|
|
789
884
|
const isL0 = reviewLevel === 'L0';
|
|
790
885
|
const autoAccept = isL0 || task.review_required === false;
|
|
791
886
|
|
|
792
|
-
const current_review = !isL0 && reviewLevel === 'L2' && task.review_required !== false
|
|
887
|
+
const current_review = !isL0 && (reviewLevel === 'L2' || reviewLevel === 'L3') && task.review_required !== false
|
|
793
888
|
? { scope: 'task', scope_id: task.id, stage: 'spec' }
|
|
794
889
|
: null;
|
|
795
890
|
const workflow_mode = current_review ? 'reviewing_task' : 'executing_task';
|
|
@@ -836,6 +931,7 @@ export async function handleExecutorResult({ result, basePath = process.cwd() }
|
|
|
836
931
|
review_level: reviewLevel,
|
|
837
932
|
current_review,
|
|
838
933
|
auto_accepted: autoAccept,
|
|
934
|
+
...(current_review ? { result_contract: RESULT_CONTRACTS.reviewer } : {}),
|
|
839
935
|
};
|
|
840
936
|
}
|
|
841
937
|
|
|
@@ -913,6 +1009,7 @@ export async function handleExecutorResult({ result, basePath = process.cwd() }
|
|
|
913
1009
|
task_id: task.id,
|
|
914
1010
|
retry_count,
|
|
915
1011
|
current_review,
|
|
1012
|
+
result_contract: shouldDebug ? RESULT_CONTRACTS.debugger : RESULT_CONTRACTS.executor,
|
|
916
1013
|
};
|
|
917
1014
|
}
|
|
918
1015
|
|
|
@@ -1032,12 +1129,21 @@ export async function handleReviewerResult({ result, basePath = process.cwd() }
|
|
|
1032
1129
|
}
|
|
1033
1130
|
}
|
|
1034
1131
|
|
|
1035
|
-
// Rework tasks
|
|
1132
|
+
// Rework tasks — persist reviewer feedback so executor knows what to fix
|
|
1036
1133
|
for (const taskId of (result.rework_tasks || [])) {
|
|
1037
1134
|
const task = getTaskById(phase, taskId);
|
|
1038
1135
|
if (!task) continue;
|
|
1039
1136
|
if (task.lifecycle === 'checkpointed' || task.lifecycle === 'accepted') {
|
|
1040
|
-
|
|
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
|
+
});
|
|
1041
1147
|
}
|
|
1042
1148
|
}
|
|
1043
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
|
|