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.
@@ -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.5",
16
+ "version": "0.5.7",
17
17
  "keywords": [
18
18
  "orchestration",
19
19
  "mcp",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "AI orchestration tool for Claude Code — GSD management shell + Superpowers quality core",
5
5
  "author": {
6
6
  "name": "sdsrss",
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gsd-lite",
3
- "version": "0.5.5",
3
+ "version": "0.5.7",
4
4
  "description": "AI orchestration tool for Claude Code — GSD management shell + Superpowers quality core",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- if (typeof issue.reason !== 'string' || issue.reason.length === 0) {
623
- errors.push('critical_issues[].reason must be non-empty string');
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: object[], important_issues: object[], minor_issues: object[], accepted_tasks: string[], rework_tasks: string[], evidence: object[]}',
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
- // Two-step transition for needs_revalidation: must go through pending first
454
- if (task.lifecycle === 'needs_revalidation') {
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.autoUnblocked.length > 0 && autoUnblock.blockers.length === 0) {
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
- taskPatches.push({ id: taskId, lifecycle: 'needs_revalidation', evidence_refs: [] });
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
 
@@ -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 { success: true };
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
- const transitionResult = validateTransition(
398
- 'phase',
399
- phase.lifecycle,
400
- 'accepted',
401
- );
402
- if (!transitionResult.valid) {
403
- return { error: true, code: ERROR_CODES.TRANSITION_ERROR, message: transitionResult.error };
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 depPhase = (state.phases || []).find(p => p.id === dep.id);
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 depPhase = (state.phases || []).find(p => p.id === dep.id);
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