ultra-builder-pro-cli 0.23.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.
- package/.ultra-template/docs/research/README.md +4 -4
- package/CHANGELOG.md +45 -0
- package/README.md +21 -18
- package/adapters/_shared/codex-assets.cjs +121 -65
- package/adapters/_shared/interaction-contract.cjs +7 -1
- package/adapters/_shared/provenance.cjs +7 -2
- package/adapters/_shared/runtime-assets.cjs +3 -2
- package/adapters/_shared/runtime-native.cjs +132 -0
- package/adapters/claude.js +60 -30
- package/adapters/codex.js +178 -81
- package/adapters/grok.js +372 -0
- package/adapters/kimi.js +7 -3
- package/adapters/opencode.js +210 -32
- package/agents/code-reviewer.md +4 -2
- package/agents/debugger.md +4 -1
- package/agents/review-code.md +7 -3
- package/agents/review-comments.md +7 -4
- package/agents/review-coordinator.md +11 -7
- package/agents/review-design.md +8 -4
- package/agents/review-errors.md +8 -4
- package/agents/review-spec.md +8 -5
- package/agents/review-tests.md +7 -4
- package/agents/tdd-runner.md +5 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +4 -13
- package/docs/AGENT-CONTEXT.md +141 -229
- package/docs/ARCHITECTURE.md +203 -494
- package/docs/ARTIFACT-AUTHORITY.md +26 -22
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
- package/docs/ROADMAP.md +85 -110
- package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
- package/docs/SKILL-AUTHORING.md +3 -3
- package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
- package/docs/WORKFLOW-LIFECYCLE.md +174 -448
- package/hooks/README.md +11 -8
- package/hooks/active_task_context.py +16 -16
- package/hooks/adapters/grok.py +134 -0
- package/hooks/{context_spine.py → context_envelope.py} +26 -22
- package/hooks/health_check.py +19 -29
- package/hooks/pre_stop_check.py +22 -12
- package/hooks/subagent_tracker.py +1 -1
- package/hooks/workflow_checkpoint.py +14 -13
- package/hooks/workflow_context.py +11 -12
- package/hooks/workflow_resume.py +14 -13
- package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
- package/mcp-server/lib/baseline-workflow.cjs +36 -38
- package/mcp-server/lib/canonical-json.cjs +19 -0
- package/mcp-server/lib/change-workflow.cjs +596 -44
- package/mcp-server/lib/context-envelope.cjs +508 -0
- package/mcp-server/lib/decision-records.cjs +337 -0
- package/mcp-server/lib/doctor.cjs +91 -33
- package/mcp-server/lib/init-project.cjs +78 -39
- package/mcp-server/lib/managed-file-write.cjs +116 -0
- package/mcp-server/lib/plan-checkpoint.cjs +177 -0
- package/mcp-server/lib/project-context.cjs +178 -0
- package/mcp-server/lib/stage-checkpoints.cjs +309 -0
- package/mcp-server/lib/state-db.cjs +155 -24
- package/mcp-server/lib/task-ledger.cjs +522 -13
- package/mcp-server/lib/ultra-facade.cjs +630 -442
- package/mcp-server/lib/worker-packet.cjs +358 -0
- package/mcp-server/server.cjs +200 -1103
- package/orchestrator/daemon.cjs +27 -9
- package/orchestrator/parallel-orchestrator.cjs +111 -76
- package/orchestrator/session-runner.cjs +135 -34
- package/package.json +10 -3
- package/skills/ultra-change/SKILL.md +9 -8
- package/skills/ultra-deliver/SKILL.md +3 -3
- package/skills/ultra-dev/SKILL.md +18 -11
- package/skills/ultra-doctor/SKILL.md +2 -2
- package/skills/ultra-init/SKILL.md +6 -5
- package/skills/ultra-init/references/brownfield-adoption.md +11 -8
- package/skills/ultra-plan/SKILL.md +10 -8
- package/skills/ultra-research/SKILL.md +12 -7
- package/skills/ultra-review/SKILL.md +9 -6
- package/skills/ultra-review/references/unified-schema.md +2 -0
- package/skills/ultra-review/scripts/review_wait.py +8 -0
- package/skills/ultra-status/SKILL.md +2 -2
- package/skills/ultra-test/SKILL.md +4 -2
- package/skills/ultra-think/SKILL.md +7 -7
- package/skills/ultra-think/references/interaction-boundary.md +56 -0
- package/spec/cli-protocol.md +80 -199
- package/spec/mcp-tools.yaml +37 -2804
- package/spec/schemas/state-db.sql +107 -4
- package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
- package/ultra-tools/commands/status.cjs +369 -243
- package/mcp-server/lib/context-spine.cjs +0 -1208
- package/mcp-server/lib/decision-dialogue.cjs +0 -1128
- package/mcp-server/lib/project-breadcrumb.cjs +0 -173
- package/mcp-server/lib/spec-learning.cjs +0 -284
- package/mcp-server/lib/workflow-state.cjs +0 -3129
- package/skills/ultra-think/references/decision-dialogue.md +0 -155
package/orchestrator/daemon.cjs
CHANGED
|
@@ -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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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)
|
|
38
|
-
|
|
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
|
-
'
|
|
41
|
+
'orchestrator execution requires an accepted Change-scoped Plan checkpoint',
|
|
44
42
|
);
|
|
45
43
|
}
|
|
46
|
-
return
|
|
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
|
|
57
|
+
WHERE id = ? AND status = 'active'`,
|
|
54
58
|
).all(changeId);
|
|
55
59
|
} else {
|
|
56
60
|
candidates = db.prepare(
|
|
57
|
-
`SELECT
|
|
58
|
-
FROM changes
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
)
|
|
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
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
179
|
+
const currentPlan = checkpoints.currentCheckpoint(
|
|
179
180
|
db,
|
|
180
|
-
|
|
181
|
-
{
|
|
181
|
+
'plan',
|
|
182
|
+
{ change_id: plan.change_id },
|
|
183
|
+
{ includeDraft: false },
|
|
182
184
|
);
|
|
183
|
-
if (
|
|
185
|
+
if (!currentPlan || checkpoints.checkpointDigest(currentPlan) !== currentPlan.digest) {
|
|
184
186
|
throw planError(
|
|
185
187
|
'PLAN_NOT_COMPLETED',
|
|
186
|
-
`change ${plan.change_id} has no
|
|
188
|
+
`change ${plan.change_id} has no current accepted Plan checkpoint`,
|
|
187
189
|
);
|
|
188
190
|
}
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
193
|
-
} catch
|
|
205
|
+
contextPayload = JSON.parse(context.payload_json);
|
|
206
|
+
} catch {
|
|
194
207
|
throw planError(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
{ authority_code:
|
|
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
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
||
|
|
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}
|
|
214
|
-
{
|
|
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
|
-
|
|
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}
|
|
239
|
+
`execution plan for ${plan.change_id} contains stale task contracts`,
|
|
221
240
|
{
|
|
222
|
-
authority_code: '
|
|
223
|
-
|
|
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
|
-
|
|
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 (!
|
|
239
|
-
||
|
|
240
|
-
||
|
|
241
|
-
||
|
|
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: '
|
|
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',
|
|
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
|
-
|
|
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
|
|
19
|
-
const
|
|
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 =
|
|
501
|
+
const dev = checkpoints.currentCheckpoint(
|
|
482
502
|
db,
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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: '
|
|
495
|
-
message: `task ${task.id} has no current
|
|
509
|
+
code: 'DEV_CHECKPOINT_NOT_ACCEPTED',
|
|
510
|
+
message: `task ${task.id} has no current accepted Dev checkpoint`,
|
|
496
511
|
};
|
|
497
512
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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:
|
|
503
|
-
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
|
|
549
|
-
'
|
|
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
|
|
577
|
+
throw new SessionRunnerError(
|
|
559
578
|
'TASK_DEPENDENCIES_INCOMPLETE',
|
|
560
579
|
`task ${task.id} has incomplete dependencies: ${incompleteDependencies.join(', ')}`,
|
|
561
580
|
);
|
|
562
581
|
}
|
|
563
|
-
|
|
564
|
-
|
|
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
|
|
575
|
-
'
|
|
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
|
|
582
|
-
'
|
|
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 =
|
|
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
|
|
598
|
-
'
|
|
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
|
-
|
|
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,
|