ultra-builder-pro-cli 0.22.0 → 0.24.0

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.
Files changed (93) hide show
  1. package/.ultra-template/docs/research/README.md +4 -4
  2. package/CHANGELOG.md +83 -0
  3. package/README.md +68 -35
  4. package/adapters/_shared/codex-assets.cjs +121 -60
  5. package/adapters/_shared/interaction-contract.cjs +20 -7
  6. package/adapters/_shared/provenance.cjs +7 -2
  7. package/adapters/_shared/runtime-assets.cjs +4 -3
  8. package/adapters/_shared/runtime-native.cjs +132 -0
  9. package/adapters/claude.js +61 -31
  10. package/adapters/codex.js +178 -81
  11. package/adapters/grok.js +372 -0
  12. package/adapters/kimi.js +8 -4
  13. package/adapters/opencode.js +211 -33
  14. package/agents/code-reviewer.md +4 -2
  15. package/agents/debugger.md +4 -1
  16. package/agents/review-code.md +7 -3
  17. package/agents/review-comments.md +7 -4
  18. package/agents/review-coordinator.md +11 -7
  19. package/agents/review-design.md +8 -4
  20. package/agents/review-errors.md +8 -4
  21. package/agents/review-spec.md +8 -5
  22. package/agents/review-tests.md +7 -4
  23. package/agents/tdd-runner.md +5 -2
  24. package/bin/install.js +9 -4
  25. package/bin/orchestrator.js +4 -13
  26. package/docs/AGENT-CONTEXT.md +141 -231
  27. package/docs/ARCHITECTURE.md +203 -498
  28. package/docs/ARTIFACT-AUTHORITY.md +38 -35
  29. package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
  30. package/docs/ROADMAP.md +85 -110
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +139 -174
  32. package/docs/SKILL-AUTHORING.md +3 -3
  33. package/docs/STATE-DB-ACCESS-POLICY.md +25 -20
  34. package/docs/WORKFLOW-LIFECYCLE.md +174 -442
  35. package/hooks/README.md +11 -8
  36. package/hooks/active_task_context.py +16 -16
  37. package/hooks/adapters/grok.py +134 -0
  38. package/hooks/{context_spine.py → context_envelope.py} +26 -22
  39. package/hooks/health_check.py +19 -29
  40. package/hooks/pre_stop_check.py +22 -12
  41. package/hooks/subagent_tracker.py +1 -1
  42. package/hooks/workflow_checkpoint.py +14 -13
  43. package/hooks/workflow_context.py +11 -12
  44. package/hooks/workflow_resume.py +14 -13
  45. package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
  46. package/mcp-server/lib/artifact-registry.cjs +25 -1
  47. package/mcp-server/lib/baseline-workflow.cjs +36 -38
  48. package/mcp-server/lib/canonical-json.cjs +19 -0
  49. package/mcp-server/lib/change-workflow.cjs +637 -49
  50. package/mcp-server/lib/context-envelope.cjs +508 -0
  51. package/mcp-server/lib/decision-records.cjs +337 -0
  52. package/mcp-server/lib/doctor.cjs +91 -33
  53. package/mcp-server/lib/init-project.cjs +78 -39
  54. package/mcp-server/lib/managed-file-write.cjs +116 -0
  55. package/mcp-server/lib/plan-checkpoint.cjs +177 -0
  56. package/mcp-server/lib/project-context.cjs +178 -0
  57. package/mcp-server/lib/stage-checkpoints.cjs +309 -0
  58. package/mcp-server/lib/state-db.cjs +155 -24
  59. package/mcp-server/lib/task-ledger.cjs +522 -13
  60. package/mcp-server/lib/ultra-facade.cjs +834 -0
  61. package/mcp-server/lib/worker-packet.cjs +358 -0
  62. package/mcp-server/server.cjs +208 -1059
  63. package/orchestrator/daemon.cjs +27 -9
  64. package/orchestrator/parallel-orchestrator.cjs +111 -76
  65. package/orchestrator/session-runner.cjs +135 -34
  66. package/package.json +10 -3
  67. package/skills/ultra-change/SKILL.md +39 -98
  68. package/skills/ultra-deliver/SKILL.md +42 -65
  69. package/skills/ultra-dev/SKILL.md +52 -99
  70. package/skills/ultra-doctor/SKILL.md +35 -57
  71. package/skills/ultra-init/SKILL.md +48 -88
  72. package/skills/ultra-init/references/brownfield-adoption.md +11 -8
  73. package/skills/ultra-plan/SKILL.md +54 -132
  74. package/skills/ultra-research/SKILL.md +58 -112
  75. package/skills/ultra-review/SKILL.md +28 -46
  76. package/skills/ultra-review/references/unified-schema.md +2 -0
  77. package/skills/ultra-review/scripts/review_wait.py +8 -0
  78. package/skills/ultra-status/SKILL.md +22 -45
  79. package/skills/ultra-test/SKILL.md +27 -61
  80. package/skills/ultra-think/SKILL.md +29 -48
  81. package/skills/ultra-think/references/interaction-boundary.md +56 -0
  82. package/spec/cli-protocol.md +80 -187
  83. package/spec/mcp-tools.yaml +130 -2738
  84. package/spec/schemas/mcp-tools.schema.json +1 -1
  85. package/spec/schemas/state-db.sql +107 -4
  86. package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
  87. package/ultra-tools/commands/status.cjs +369 -243
  88. package/mcp-server/lib/context-spine.cjs +0 -1208
  89. package/mcp-server/lib/decision-dialogue.cjs +0 -1128
  90. package/mcp-server/lib/project-breadcrumb.cjs +0 -173
  91. package/mcp-server/lib/spec-learning.cjs +0 -284
  92. package/mcp-server/lib/workflow-state.cjs +0 -2965
  93. package/skills/ultra-think/references/decision-dialogue.md +0 -153
@@ -14,6 +14,7 @@
14
14
  const ops = require('../mcp-server/lib/state-ops.cjs');
15
15
  const runtimeState = require('../mcp-server/lib/runtime-state.cjs');
16
16
  const projector = require('../mcp-server/lib/projector.cjs');
17
+ const workerPackets = require('../mcp-server/lib/worker-packet.cjs');
17
18
  const runner = require('./session-runner.cjs');
18
19
  const recovery = require('./recovery.cjs');
19
20
  const { evaluate, DEFAULT_RULES } = require('./dispatch-rules.cjs');
@@ -184,14 +185,10 @@ function runDaemon({
184
185
  if (task.stale) continue;
185
186
  if (!taskDependenciesReady(db, task)) continue;
186
187
  if (hasActiveFileConflict(db, task)) continue;
187
- try {
188
- runner.assertSessionTaskReady(db, repoRoot, task.id);
189
- } catch (error) {
190
- // A normal workflow gate is a durable pending state, not a daemon
191
- // failure and not an error to repeat on every poll.
192
- if (!runner.isExpectedExecutionGate(error) && onError) onError(error);
193
- continue;
194
- }
188
+ // v0.24 workers are always bound to an active Change and receive one
189
+ // immutable Worker Packet. Legacy unbound tasks remain readable but are
190
+ // never dispatched by the daemon.
191
+ if (!task.change_id) continue;
195
192
  // admissionCheck catches both live-session conflicts and tripped breakers.
196
193
  let verdict;
197
194
  try { verdict = ops.admissionCheck(db, task.id); }
@@ -201,7 +198,21 @@ function runDaemon({
201
198
  const runtime = routeTask(task, runtimes);
202
199
  if (!runtime) continue;
203
200
 
201
+ let packet = null;
204
202
  try {
203
+ packet = workerPackets.createWorkerPacket(db, {
204
+ role: 'implement',
205
+ task_id: task.id,
206
+ runtime,
207
+ output_path: `.ultra/changes/active/${task.change_id}/delivery/${task.id}-outcome.json`,
208
+ output_schema: {
209
+ type: 'object',
210
+ required: ['packet_digest'],
211
+ properties: {
212
+ packet_digest: { type: 'string', pattern: '^[0-9a-f]{64}$' },
213
+ },
214
+ },
215
+ }, { rootDir: repoRoot });
205
216
  const handle = runner.spawnSession({
206
217
  db, repoRoot,
207
218
  task_id: task.id,
@@ -209,12 +220,19 @@ function runDaemon({
209
220
  command,
210
221
  args: commandArgs,
211
222
  mark_task_started: true,
223
+ kernel_mode: true,
224
+ packet_digest: packet.packet_digest,
212
225
  });
213
226
  handle.task_id = task.id;
227
+ handle.packet = packet;
228
+ workerPackets.markWorkerPacketAssigned(db, packet.id);
214
229
  settleExecution(handle, task);
215
230
  children.push(handle);
216
231
  } catch (err) {
217
- if (onError) onError(err);
232
+ if (packet?.id) {
233
+ workerPackets.abandonWorkerPacket(db, packet.id, err.code || err.message);
234
+ }
235
+ if (!runner.isExpectedExecutionGate(err) && onError) onError(err);
218
236
  // Don't throw — one bad task shouldn't stop the loop.
219
237
  }
220
238
  }
@@ -2,13 +2,13 @@
2
2
 
3
3
  // Phase 8B.2 — Parallel orchestrator.
4
4
  //
5
- // Consumes the current Change-scoped plan.json (built by the plan workflow) and
5
+ // Consumes the current Change-scoped plan.json (published by a Plan checkpoint) and
6
6
  // drives resumable per-wave session spawning. Each wave is either parallel
7
7
  // (batch Promise.all on proc exit) or serial (await per task) based on the
8
8
  // file-overlap detection already done by plan-builder.
9
9
  //
10
10
  // Responsibilities:
11
- // • resolve the canonical Change plan (or an explicit legacy object/path)
11
+ // • resolve the canonical accepted Change plan
12
12
  // • for each ready wave: evaluate dispatch-rules per task → spawn sessions
13
13
  // via session-runner → wait for transport exit → preserve task-gate state
14
14
  // • stop at the first non-converged wave and resume safely on the next call
@@ -25,43 +25,47 @@ const path = require('node:path');
25
25
  const { isDeepStrictEqual } = require('node:util');
26
26
 
27
27
  const planStore = require('../mcp-server/lib/plan-store.cjs');
28
- const contextSpine = require('../mcp-server/lib/context-spine.cjs');
29
28
  const ops = require('../mcp-server/lib/state-ops.cjs');
30
- const workflows = require('../mcp-server/lib/workflow-state.cjs');
29
+ const checkpoints = require('../mcp-server/lib/stage-checkpoints.cjs');
30
+ const workerPackets = require('../mcp-server/lib/worker-packet.cjs');
31
31
  const runner = require('./session-runner.cjs');
32
32
  const dispatchRules = require('./dispatch-rules.cjs');
33
33
 
34
34
  const TERMINAL_TASK_STATUSES = new Set(['completed', 'expanded']);
35
35
 
36
36
  function resolvePlan({ db, plan, planPath, repoRoot, changeId = null }) {
37
- if (plan) return plan;
38
- if (planPath) {
39
- const legacy = JSON.parse(fs.readFileSync(planPath, 'utf8'));
40
- if (legacy?.change_id) {
37
+ if (plan) {
38
+ if (!plan.change_id) {
41
39
  throw planError(
42
40
  'PLAN_CHANGE_SCOPED_REQUIRED',
43
- 'change-bound plans must be resolved from their canonical Change artifact root',
41
+ 'orchestrator execution requires an accepted Change-scoped Plan checkpoint',
44
42
  );
45
43
  }
46
- return legacy;
44
+ return plan;
45
+ }
46
+ if (planPath) {
47
+ throw planError(
48
+ 'PLAN_LEGACY_RETIRED',
49
+ 'explicit planPath execution is retired; migrate the project and use its accepted Change Plan',
50
+ );
47
51
  }
48
52
  let candidates;
49
53
  if (changeId) {
50
54
  candidates = db.prepare(
51
55
  `SELECT id, artifact_root, status
52
56
  FROM changes
53
- WHERE id = ? AND status IN ('active', 'blocked')`,
57
+ WHERE id = ? AND status = 'active'`,
54
58
  ).all(changeId);
55
59
  } else {
56
60
  candidates = db.prepare(
57
- `SELECT c.id, c.artifact_root, c.status, MAX(w.completed_at) AS planned_at
58
- FROM changes c
59
- JOIN workflow_runs w ON w.change_id = c.id
60
- WHERE c.status IN ('active', 'blocked')
61
- AND w.kind = 'plan' AND w.status = 'completed'
62
- GROUP BY c.id, c.artifact_root, c.status
63
- ORDER BY planned_at DESC, c.id ASC`,
64
- ).all();
61
+ `SELECT id, artifact_root, status
62
+ FROM changes WHERE status = 'active' ORDER BY updated_at DESC, id ASC`,
63
+ ).all().filter((candidate) => checkpoints.currentCheckpoint(
64
+ db,
65
+ 'plan',
66
+ { change_id: candidate.id },
67
+ { includeDraft: false },
68
+ ));
65
69
  }
66
70
  if (candidates.length > 1) {
67
71
  throw planError(
@@ -78,7 +82,7 @@ function resolvePlan({ db, plan, planPath, repoRoot, changeId = null }) {
78
82
  const err = new Error(
79
83
  changeId
80
84
  ? `runPlan: no current scoped plan found for change ${changeId}`
81
- : 'runPlan: no current Change-scoped plan found; pass an explicit legacy planPath only for migration compatibility',
85
+ : 'runPlan: no current accepted Change-scoped Plan found',
82
86
  );
83
87
  err.code = 'PLAN_NOT_FOUND';
84
88
  throw err;
@@ -159,13 +163,10 @@ function assertPlanAuthority(db, repoRoot, plan) {
159
163
  const taskChangeIds = new Set(taskRows.map((task) => task.change_id).filter(Boolean));
160
164
 
161
165
  if (!plan.change_id) {
162
- if (taskChangeIds.size > 0) {
163
- throw planError(
164
- 'PLAN_NOT_COMPLETED',
165
- 'change-owned tasks require a change-bound, completed current execution plan',
166
- );
167
- }
168
- return { mode: 'legacy-unbound', workflow_id: null };
166
+ throw planError(
167
+ 'PLAN_CHANGE_SCOPED_REQUIRED',
168
+ 'orchestrator execution requires an accepted Change-scoped Plan checkpoint',
169
+ );
169
170
  }
170
171
  if (taskChangeIds.size > 1 || (taskChangeIds.size === 1 && !taskChangeIds.has(plan.change_id))) {
171
172
  throw planError(
@@ -175,74 +176,89 @@ function assertPlanAuthority(db, repoRoot, plan) {
175
176
  );
176
177
  }
177
178
 
178
- const completedRuns = workflows.listWorkflows(
179
+ const currentPlan = checkpoints.currentCheckpoint(
179
180
  db,
180
- { kind: 'plan', status: 'completed', change_id: plan.change_id, limit: 100 },
181
- { rootDir: repoRoot },
181
+ 'plan',
182
+ { change_id: plan.change_id },
183
+ { includeDraft: false },
182
184
  );
183
- if (completedRuns.length === 0) {
185
+ if (!currentPlan || checkpoints.checkpointDigest(currentPlan) !== currentPlan.digest) {
184
186
  throw planError(
185
187
  'PLAN_NOT_COMPLETED',
186
- `change ${plan.change_id} has no completed plan workflow`,
188
+ `change ${plan.change_id} has no current accepted Plan checkpoint`,
187
189
  );
188
190
  }
189
-
190
- let currentPlan;
191
+ const context = currentPlan.context_envelope_id
192
+ ? db.prepare('SELECT * FROM context_envelopes WHERE id = ?')
193
+ .get(currentPlan.context_envelope_id)
194
+ : null;
195
+ if (!context || context.stage !== 'plan' || context.scope_type !== 'change'
196
+ || context.scope_id !== plan.change_id) {
197
+ throw planError(
198
+ 'PLAN_STALE',
199
+ `execution plan for ${plan.change_id} is not bound to its Context Envelope`,
200
+ { authority_code: 'PLAN_CONTEXT_MISMATCH' },
201
+ );
202
+ }
203
+ let contextPayload;
191
204
  try {
192
- currentPlan = workflows.assertCurrentPlan(db, plan.change_id, repoRoot);
193
- } catch (error) {
205
+ contextPayload = JSON.parse(context.payload_json);
206
+ } catch {
194
207
  throw planError(
195
- error.code === 'WORKFLOW_PLAN_NOT_COMPLETED' ? 'PLAN_NOT_COMPLETED' : 'PLAN_STALE',
196
- error.message,
197
- { authority_code: error.code || null },
208
+ 'PLAN_STALE',
209
+ `execution plan for ${plan.change_id} has a corrupt Context Envelope`,
210
+ { authority_code: 'PLAN_CONTEXT_INVALID' },
198
211
  );
199
212
  }
200
- const currentContext = contextSpine.validateContextSnapshot(db, {
201
- change_id: plan.change_id,
202
- task_id: null,
203
- role: 'plan',
204
- gate: 'planning',
205
- }, {
206
- rootDir: repoRoot,
207
- require_current_checkout: true,
208
- });
209
- if (!currentContext.snapshot
210
- || currentContext.snapshot.id !== currentPlan.plan.summary.plan_context_snapshot_id) {
213
+ const baseline = db.prepare(
214
+ `SELECT scope_json FROM baselines
215
+ WHERE status <> 'superseded' ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
216
+ ).get();
217
+ const scope = baseline
218
+ ? JSON.parse(baseline.scope_json || '["."]')
219
+ : ['.'];
220
+ const currentCheckout = require('../mcp-server/lib/baseline-workflow.cjs')
221
+ .gitWorktreeSnapshot(repoRoot, scope);
222
+ const plannedWorktreeDigest = contextPayload.git?.scoped_worktree_digest
223
+ || contextPayload.git?.worktree_digest
224
+ || null;
225
+ if (plannedWorktreeDigest && currentCheckout.digest !== plannedWorktreeDigest) {
211
226
  throw planError(
212
227
  'PLAN_STALE',
213
- `execution plan for ${plan.change_id} was superseded by another planning context`,
214
- { authority_code: 'WORKFLOW_PLAN_CONTEXT_MISMATCH' },
228
+ `execution plan for ${plan.change_id} no longer matches the scoped checkout`,
229
+ {
230
+ authority_code: 'PLAN_CONTEXT_STALE',
231
+ blockers: ['CONTEXT_WORKTREE_STALE'],
232
+ },
215
233
  );
216
234
  }
217
- if (currentContext.blockers.length > 0) {
235
+ const staleTasks = taskRows.filter((task) => task.stale);
236
+ if (staleTasks.length > 0) {
218
237
  throw planError(
219
238
  'PLAN_STALE',
220
- `execution plan for ${plan.change_id} has stale planning context`,
239
+ `execution plan for ${plan.change_id} contains stale task contracts`,
221
240
  {
222
- authority_code: 'WORKFLOW_PLAN_CONTEXT_STALE',
223
- blockers: currentContext.blockers,
241
+ authority_code: 'PLAN_TASK_CONTRACT_STALE',
242
+ task_ids: staleTasks.map((task) => task.id),
224
243
  },
225
244
  );
226
245
  }
227
- const expected = planStore.buildPlan(
228
- currentPlan.tasks,
229
- { changeId: plan.change_id },
230
- );
231
- const { context, ...planCore } = plan;
246
+ const expected = planStore.buildPlan(taskRows, { changeId: plan.change_id });
247
+ const { context: planContext, ...planCore } = plan;
232
248
  if (!isDeepStrictEqual(planCore, expected)) {
233
249
  throw planError(
234
250
  'PLAN_STALE',
235
251
  `execution plan for ${plan.change_id} does not match the current DB task graph`,
236
252
  );
237
253
  }
238
- if (!context
239
- || context.snapshot_id !== currentPlan.plan.summary.plan_context_snapshot_id
240
- || context.manifest_path !== currentPlan.plan.summary.plan_context_path
241
- || context.manifest_digest !== currentPlan.plan.summary.plan_context_digest) {
254
+ if (!planContext
255
+ || planContext.snapshot_id !== context.id
256
+ || planContext.manifest_path !== context.artifact_path
257
+ || planContext.manifest_digest !== context.digest) {
242
258
  throw planError(
243
259
  'PLAN_STALE',
244
260
  `execution plan for ${plan.change_id} does not match its planning context`,
245
- { authority_code: 'WORKFLOW_PLAN_CONTEXT_MISMATCH' },
261
+ { authority_code: 'PLAN_CONTEXT_MISMATCH' },
246
262
  );
247
263
  }
248
264
  const change = db.prepare(
@@ -251,14 +267,6 @@ function assertPlanAuthority(db, repoRoot, plan) {
251
267
  const canonicalPaths = planStore.changePlanPaths(repoRoot, change);
252
268
  const expectedPlanPath = path.relative(repoRoot, canonicalPaths.json);
253
269
  const expectedPlanMdPath = path.relative(repoRoot, canonicalPaths.md);
254
- if (currentPlan.plan.summary.plan_path !== expectedPlanPath
255
- || currentPlan.plan.summary.plan_md_path !== expectedPlanMdPath) {
256
- throw planError(
257
- 'PLAN_STALE',
258
- `completed plan workflow for ${plan.change_id} is not bound to its canonical artifacts`,
259
- { authority_code: 'WORKFLOW_PLAN_ARTIFACT_INVALID' },
260
- );
261
- }
262
270
  const canonicalPlan = planStore.loadChangePlanArtifact(
263
271
  repoRoot,
264
272
  change,
@@ -270,7 +278,7 @@ function assertPlanAuthority(db, repoRoot, plan) {
270
278
  `execution plan for ${plan.change_id} differs from its canonical Change artifact`,
271
279
  );
272
280
  }
273
- return { mode: 'current-change', workflow_id: currentPlan.plan.id };
281
+ return { mode: 'current-change', checkpoint_id: currentPlan.id };
274
282
  }
275
283
 
276
284
  function awaitExit(proc) {
@@ -451,7 +459,27 @@ function reservePreparedTask(db, repoRoot, prepared, ctx) {
451
459
  } = prepared;
452
460
  const { onError } = ctx;
453
461
  let handle;
462
+ let packet = null;
454
463
  try {
464
+ if (!task.change_id) {
465
+ throw planError(
466
+ 'TASK_CHANGE_REQUIRED',
467
+ `orchestrator task ${task.id} must belong to the accepted Change`,
468
+ );
469
+ }
470
+ packet = workerPackets.createWorkerPacket(db, {
471
+ role: 'implement',
472
+ task_id: task.id,
473
+ runtime: decision.runtime,
474
+ output_path: `.ultra/changes/active/${task.change_id}/delivery/${task.id}-outcome.json`,
475
+ output_schema: {
476
+ type: 'object',
477
+ required: ['packet_digest'],
478
+ properties: {
479
+ packet_digest: { type: 'string', pattern: '^[0-9a-f]{64}$' },
480
+ },
481
+ },
482
+ }, { rootDir: repoRoot });
455
483
  handle = runner.spawnSession({
456
484
  db, repoRoot,
457
485
  task_id: task.id,
@@ -459,9 +487,16 @@ function reservePreparedTask(db, repoRoot, prepared, ctx) {
459
487
  command: ctx.deferStart ? null : command,
460
488
  args: ctx.deferStart ? [] : args,
461
489
  mark_task_started: true,
490
+ kernel_mode: true,
491
+ packet_digest: packet.packet_digest,
462
492
  });
463
493
  handle.task_id = task.id;
494
+ workerPackets.markWorkerPacketAssigned(db, packet.id);
495
+ handle.packet = packet;
464
496
  } catch (err) {
497
+ if (packet?.id) {
498
+ workerPackets.abandonWorkerPacket(db, packet.id, err.code || err.message);
499
+ }
465
500
  const authorityBlocked = authorityBlockedResult(err, task.id, {
466
501
  reservationFailed: true,
467
502
  });
@@ -755,7 +790,7 @@ async function runPlan({
755
790
  waves: resolved.waves.length,
756
791
  waves_completed: completedWaves,
757
792
  paused_wave_id: pausedWave?.id || null,
758
- plan_workflow_id: planAuthority.workflow_id,
793
+ plan_checkpoint_id: planAuthority.checkpoint_id || null,
759
794
  awaiting_workflow_gates: awaitingWorkflowGates,
760
795
  results: allResults,
761
796
  };
@@ -15,12 +15,22 @@ const { spawn, execFileSync } = require('node:child_process');
15
15
  const { randomUUID } = require('node:crypto');
16
16
 
17
17
  const ops = require('../mcp-server/lib/state-ops.cjs');
18
- const workflows = require('../mcp-server/lib/workflow-state.cjs');
19
- const contextSpine = require('../mcp-server/lib/context-spine.cjs');
18
+ const checkpoints = require('../mcp-server/lib/stage-checkpoints.cjs');
19
+ const workerPackets = require('../mcp-server/lib/worker-packet.cjs');
20
20
  const runtimePaths = require('../mcp-server/lib/runtime-paths.cjs');
21
21
  const gitBootstrap = require('../mcp-server/lib/git-bootstrap.cjs');
22
22
  const closeJournal = require('./session-close-journal.cjs');
23
23
 
24
+ function legacyContextSpine() {
25
+ return module.require(path.join(
26
+ __dirname,
27
+ '..',
28
+ 'mcp-server',
29
+ 'lib',
30
+ 'context-spine.cjs',
31
+ ));
32
+ }
33
+
24
34
  const EXPECTED_EXECUTION_GATE_CODES = new Set([
25
35
  'TASK_EXECUTION_CONTRACT_INCOMPLETE',
26
36
  'WORKFLOW_TASK_NOT_EXECUTABLE',
@@ -31,6 +41,15 @@ const EXPECTED_EXECUTION_GATE_CODES = new Set([
31
41
  'WORKFLOW_PLAN_CONTEXT_STALE',
32
42
  'WORKFLOW_IMPLEMENTATION_CONTEXT_REQUIRED',
33
43
  'WORKFLOW_IMPLEMENTATION_CONTEXT_STALE',
44
+ 'TASK_NOT_EXECUTABLE',
45
+ 'PLAN_CHECKPOINT_REQUIRED',
46
+ 'PLAN_TASK_CONTRACT_MISSING',
47
+ 'PLAN_TASK_CONTRACT_STALE',
48
+ 'IMPLEMENTATION_CONTEXT_REQUIRED',
49
+ 'IMPLEMENTATION_CONTEXT_STALE',
50
+ 'PLAN_CONTEXT_STALE',
51
+ 'WORKER_PACKET_REQUIRED',
52
+ 'CONTEXT_REQUIRED_REF_STALE',
34
53
  'ADMISSION_DENIED',
35
54
  'ACTIVE_SESSION_LEASE_CONFLICT',
36
55
  'SESSION_CLOSE_PENDING',
@@ -47,11 +66,12 @@ const EXPECTED_EXECUTION_GATE_CODES = new Set([
47
66
  ]);
48
67
 
49
68
  class SessionRunnerError extends Error {
50
- constructor(code, message, { cause } = {}) {
69
+ constructor(code, message, { cause, details } = {}) {
51
70
  super(message);
52
71
  this.name = 'SessionRunnerError';
53
72
  this.code = code;
54
73
  if (cause) this.cause = cause;
74
+ if (details !== undefined) this.details = details;
55
75
  }
56
76
  }
57
77
 
@@ -478,29 +498,28 @@ function changeTaskIntegrationBlocker(db, task, worktreePath) {
478
498
  message: `task ${task.id} completion commit does not match worktree HEAD`,
479
499
  };
480
500
  }
481
- const dev = workflows.listWorkflows(
501
+ const dev = checkpoints.currentCheckpoint(
482
502
  db,
483
- {
484
- kind: 'dev',
485
- status: 'ready',
486
- change_id: task.change_id,
487
- task_id: task.id,
488
- limit: 1,
489
- },
490
- { rootDir: worktreePath },
491
- )[0];
492
- if (!dev || dev.artifact_health.status !== 'pass') {
503
+ 'dev',
504
+ { task_id: task.id },
505
+ { includeDraft: false },
506
+ );
507
+ if (!dev || checkpoints.checkpointDigest(dev) !== dev.digest) {
493
508
  return {
494
- code: 'DEV_WORKFLOW_NOT_READY',
495
- message: `task ${task.id} has no current ready dev workflow`,
509
+ code: 'DEV_CHECKPOINT_NOT_ACCEPTED',
510
+ message: `task ${task.id} has no current accepted Dev checkpoint`,
496
511
  };
497
512
  }
498
- try {
499
- workflows.assertApprovedReview(db, task.change_id, task.id, worktreePath);
500
- } catch (error) {
513
+ const review = checkpoints.currentCheckpoint(
514
+ db,
515
+ 'review',
516
+ { task_id: task.id },
517
+ { includeDraft: false },
518
+ );
519
+ if (!review || checkpoints.checkpointDigest(review) !== review.digest) {
501
520
  return {
502
- code: error.code || 'TASK_REVIEW_NOT_CURRENT',
503
- message: error.message,
521
+ code: 'REVIEW_CHECKPOINT_NOT_ACCEPTED',
522
+ message: `task ${task.id} has no current accepted Review checkpoint`,
504
523
  };
505
524
  }
506
525
  return null;
@@ -545,8 +564,8 @@ function assertSessionTaskReady(db, repoRoot, taskId) {
545
564
  if (!task.change_id) return task;
546
565
  ops.assertTaskExecutionContract(task);
547
566
  if (!['pending', 'in_progress', 'blocked'].includes(task.status) || task.stale) {
548
- throw new workflows.WorkflowStateError(
549
- 'WORKFLOW_TASK_NOT_EXECUTABLE',
567
+ throw new SessionRunnerError(
568
+ 'TASK_NOT_EXECUTABLE',
550
569
  `task ${task.id} is not executable from status ${task.status}${task.stale ? ' (stale)' : ''}`,
551
570
  );
552
571
  }
@@ -555,13 +574,24 @@ function assertSessionTaskReady(db, repoRoot, taskId) {
555
574
  return !dependency || !['completed', 'expanded'].includes(dependency.status);
556
575
  });
557
576
  if (incompleteDependencies.length > 0) {
558
- throw new workflows.WorkflowStateError(
577
+ throw new SessionRunnerError(
559
578
  'TASK_DEPENDENCIES_INCOMPLETE',
560
579
  `task ${task.id} has incomplete dependencies: ${incompleteDependencies.join(', ')}`,
561
580
  );
562
581
  }
563
- workflows.assertCurrentPlan(db, task.change_id, repoRoot);
564
- const implementationContext = contextSpine.validateContextSnapshot(db, {
582
+ const plan = checkpoints.currentCheckpoint(
583
+ db,
584
+ 'plan',
585
+ { change_id: task.change_id },
586
+ { includeDraft: false },
587
+ );
588
+ if (!plan || checkpoints.checkpointDigest(plan) !== plan.digest) {
589
+ throw new SessionRunnerError(
590
+ 'PLAN_CHECKPOINT_REQUIRED',
591
+ `task ${task.id} requires an accepted Plan checkpoint`,
592
+ );
593
+ }
594
+ const implementationContext = legacyContextSpine().validateContextSnapshot(db, {
565
595
  change_id: task.change_id,
566
596
  task_id: task.id,
567
597
  role: 'implement',
@@ -571,20 +601,20 @@ function assertSessionTaskReady(db, repoRoot, taskId) {
571
601
  require_current_checkout: true,
572
602
  });
573
603
  if (!implementationContext.snapshot) {
574
- throw new workflows.WorkflowStateError(
575
- 'WORKFLOW_IMPLEMENTATION_CONTEXT_REQUIRED',
604
+ throw new SessionRunnerError(
605
+ 'IMPLEMENTATION_CONTEXT_REQUIRED',
576
606
  `task ${task.id} requires a ready implementation Context before session admission`,
577
607
  { blockers: implementationContext.blockers },
578
608
  );
579
609
  }
580
610
  if (implementationContext.blockers.length > 0) {
581
- throw new workflows.WorkflowStateError(
582
- 'WORKFLOW_IMPLEMENTATION_CONTEXT_STALE',
611
+ throw new SessionRunnerError(
612
+ 'IMPLEMENTATION_CONTEXT_STALE',
583
613
  `task ${task.id} implementation Context is stale`,
584
614
  { blockers: implementationContext.blockers },
585
615
  );
586
616
  }
587
- const planningContext = contextSpine.validateContextSnapshot(db, {
617
+ const planningContext = legacyContextSpine().validateContextSnapshot(db, {
588
618
  change_id: task.change_id,
589
619
  task_id: null,
590
620
  role: 'plan',
@@ -594,8 +624,8 @@ function assertSessionTaskReady(db, repoRoot, taskId) {
594
624
  require_current_checkout: true,
595
625
  });
596
626
  if (!planningContext.snapshot || planningContext.blockers.length > 0) {
597
- throw new workflows.WorkflowStateError(
598
- 'WORKFLOW_PLAN_CONTEXT_STALE',
627
+ throw new SessionRunnerError(
628
+ 'PLAN_CONTEXT_STALE',
599
629
  `change ${task.change_id} planning Context is stale at session admission`,
600
630
  { blockers: planningContext.blockers },
601
631
  );
@@ -603,6 +633,73 @@ function assertSessionTaskReady(db, repoRoot, taskId) {
603
633
  return task;
604
634
  }
605
635
 
636
+ function assertKernelSessionTaskReady(db, repoRoot, taskId, packetDigest) {
637
+ const task = ops.readTask(db, taskId);
638
+ if (!task) {
639
+ throw new SessionRunnerError('TASK_NOT_FOUND', `task ${taskId} not found`);
640
+ }
641
+ ops.assertTaskExecutionContract(task);
642
+ if (!['pending', 'in_progress', 'blocked'].includes(task.status) || task.stale) {
643
+ throw new SessionRunnerError(
644
+ 'TASK_NOT_EXECUTABLE',
645
+ `task ${task.id} is not executable from status ${task.status}${task.stale ? ' (stale)' : ''}`,
646
+ );
647
+ }
648
+ const incompleteDependencies = (task.deps || []).filter((id) => {
649
+ const dependency = ops.readTask(db, id);
650
+ return !dependency || !['completed', 'expanded'].includes(dependency.status);
651
+ });
652
+ if (incompleteDependencies.length > 0) {
653
+ throw new SessionRunnerError(
654
+ 'TASK_DEPENDENCIES_INCOMPLETE',
655
+ `task ${task.id} has incomplete dependencies: ${incompleteDependencies.join(', ')}`,
656
+ );
657
+ }
658
+ if (task.change_id) {
659
+ const plan = checkpoints.currentCheckpoint(
660
+ db,
661
+ 'plan',
662
+ { change_id: task.change_id },
663
+ { includeDraft: false },
664
+ );
665
+ if (!plan) {
666
+ throw new SessionRunnerError(
667
+ 'PLAN_CHECKPOINT_REQUIRED',
668
+ `task ${task.id} requires an accepted Plan checkpoint`,
669
+ );
670
+ }
671
+ const plannedDigest = plan.payload?.plan?.task_contract_digests?.[task.id];
672
+ if (!plannedDigest) {
673
+ throw new SessionRunnerError(
674
+ 'PLAN_TASK_CONTRACT_MISSING',
675
+ `accepted Plan checkpoint does not bind task ${task.id}`,
676
+ );
677
+ }
678
+ const packet = packetDigest
679
+ ? db.prepare(
680
+ `SELECT id, task_digest FROM worker_packets
681
+ WHERE packet_digest = ? AND scope_type = 'task' AND scope_id = ?
682
+ AND status IN ('pending', 'assigned')`,
683
+ ).get(packetDigest, task.id)
684
+ : null;
685
+ if (!packet) {
686
+ throw new SessionRunnerError(
687
+ 'WORKER_PACKET_REQUIRED',
688
+ `task ${task.id} requires the exact Worker Packet prepared for this session`,
689
+ );
690
+ }
691
+ workerPackets.readWorkerPacket(db, packet.id, { rootDir: repoRoot });
692
+ if (packet.task_digest !== plannedDigest) {
693
+ throw new SessionRunnerError(
694
+ 'PLAN_TASK_CONTRACT_STALE',
695
+ `task ${task.id} no longer matches its accepted Plan checkpoint`,
696
+ );
697
+ }
698
+ return task;
699
+ }
700
+ return task;
701
+ }
702
+
606
703
  function admissionCheck(db, repoRoot, taskId, options = {}) {
607
704
  assertSessionTaskReady(db, repoRoot, taskId);
608
705
  const pendingClose = closeJournal.findForTask(repoRoot, taskId);
@@ -697,6 +794,8 @@ function spawnSession({
697
794
  mark_task_started = false,
698
795
  takeover_timeout_ms = 5000,
699
796
  takeover_poll_ms = 25,
797
+ kernel_mode = false,
798
+ packet_digest = null,
700
799
  }) {
701
800
  if (!db) throw new SessionRunnerError('VALIDATION_ERROR', 'db handle required');
702
801
  if (!repoRoot) throw new SessionRunnerError('VALIDATION_ERROR', 'repoRoot required');
@@ -717,7 +816,8 @@ function spawnSession({
717
816
  }
718
817
  // Validate workflow authority before takeover can terminate the prior worker
719
818
  // or any filesystem mutation creates a new worktree.
720
- assertSessionTaskReady(db, repoRoot, task_id);
819
+ if (kernel_mode) assertKernelSessionTaskReady(db, repoRoot, task_id, packet_digest);
820
+ else assertSessionTaskReady(db, repoRoot, task_id);
721
821
  let authority;
722
822
  try {
723
823
  authority = runtimePaths.ensureRuntimeState(repoRoot, {
@@ -1140,6 +1240,7 @@ module.exports = {
1140
1240
  attachHeartbeat,
1141
1241
  admissionCheck,
1142
1242
  assertSessionTaskReady,
1243
+ assertKernelSessionTaskReady,
1143
1244
  // exposed for tests
1144
1245
  _internal: {
1145
1246
  gitWorktreeAdd,