ultra-builder-pro-cli 0.22.0 → 0.23.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/CHANGELOG.md +38 -0
- package/README.md +55 -25
- package/adapters/_shared/codex-assets.cjs +8 -3
- package/adapters/_shared/interaction-contract.cjs +13 -6
- package/adapters/_shared/runtime-assets.cjs +1 -1
- package/adapters/claude.js +1 -1
- package/adapters/kimi.js +1 -1
- package/adapters/opencode.js +1 -1
- package/docs/AGENT-CONTEXT.md +38 -40
- package/docs/ARCHITECTURE.md +29 -33
- package/docs/ARTIFACT-AUTHORITY.md +15 -16
- package/docs/ROADMAP.md +2 -2
- package/docs/RUNTIME-COMPAT-MATRIX.md +7 -6
- package/docs/STATE-DB-ACCESS-POLICY.md +14 -14
- package/docs/WORKFLOW-LIFECYCLE.md +39 -33
- package/mcp-server/lib/artifact-registry.cjs +25 -1
- package/mcp-server/lib/change-workflow.cjs +42 -6
- package/mcp-server/lib/ultra-facade.cjs +646 -0
- package/mcp-server/lib/workflow-state.cjs +166 -2
- package/mcp-server/server.cjs +73 -21
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +38 -98
- package/skills/ultra-deliver/SKILL.md +42 -65
- package/skills/ultra-dev/SKILL.md +34 -88
- package/skills/ultra-doctor/SKILL.md +35 -57
- package/skills/ultra-init/SKILL.md +37 -78
- package/skills/ultra-plan/SKILL.md +52 -132
- package/skills/ultra-research/SKILL.md +41 -100
- package/skills/ultra-review/SKILL.md +25 -46
- package/skills/ultra-status/SKILL.md +21 -44
- package/skills/ultra-test/SKILL.md +25 -61
- package/skills/ultra-think/SKILL.md +29 -48
- package/skills/ultra-think/references/decision-dialogue.md +6 -4
- package/spec/cli-protocol.md +12 -0
- package/spec/mcp-tools.yaml +159 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('node:path');
|
|
4
|
+
|
|
5
|
+
const ops = require('./state-ops.cjs');
|
|
6
|
+
const baselines = require('./baseline-workflow.cjs');
|
|
7
|
+
const changes = require('./change-workflow.cjs');
|
|
8
|
+
const workflows = require('./workflow-state.cjs');
|
|
9
|
+
const doctor = require('./doctor.cjs');
|
|
10
|
+
const taskLedger = require('./task-ledger.cjs');
|
|
11
|
+
|
|
12
|
+
const PUBLIC_TOOLS = Object.freeze([
|
|
13
|
+
'ultra.context',
|
|
14
|
+
'ultra.record',
|
|
15
|
+
'ultra.checkpoint',
|
|
16
|
+
'ultra.sync',
|
|
17
|
+
'ultra.session',
|
|
18
|
+
'ultra.archive',
|
|
19
|
+
'ultra.doctor',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const RECORD_OPERATIONS = new Set([
|
|
23
|
+
'task.init_project',
|
|
24
|
+
'task.create',
|
|
25
|
+
'task.update',
|
|
26
|
+
'task.delete',
|
|
27
|
+
'task.append_event',
|
|
28
|
+
'baseline.start',
|
|
29
|
+
'baseline.record',
|
|
30
|
+
'baseline.converge',
|
|
31
|
+
'change.create',
|
|
32
|
+
'change.update',
|
|
33
|
+
'change.delta',
|
|
34
|
+
'change.documentation_reconcile',
|
|
35
|
+
'change.context',
|
|
36
|
+
'change.learning_propose',
|
|
37
|
+
'change.learning_resolve',
|
|
38
|
+
'decision.thread_start',
|
|
39
|
+
'decision.open',
|
|
40
|
+
'decision.resolve',
|
|
41
|
+
'decision.delegate',
|
|
42
|
+
'decision.defer',
|
|
43
|
+
'decision.supersede',
|
|
44
|
+
'decision.complete',
|
|
45
|
+
'decision.checkpoint',
|
|
46
|
+
'artifact.record',
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
const STAGE_CONTEXT = Object.freeze({
|
|
50
|
+
plan: { role: 'plan', gate: 'planning' },
|
|
51
|
+
dev: { role: 'implement', gate: 'implementation' },
|
|
52
|
+
test: { role: 'check', gate: 'verification' },
|
|
53
|
+
review: { role: 'review', gate: 'review' },
|
|
54
|
+
deliver: { role: 'check', gate: 'convergence' },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const HARD_ERROR_CODES = new Set([
|
|
58
|
+
'STATE_CORRUPT',
|
|
59
|
+
'STATE_DB_ERROR',
|
|
60
|
+
'STATE_DB_MISSING',
|
|
61
|
+
'SCHEMA_VERSION_MISMATCH',
|
|
62
|
+
'PATH_AUTHORITY_VIOLATION',
|
|
63
|
+
'ARCHIVE_PATH_UNSAFE',
|
|
64
|
+
'ARCHIVE_RUNTIME_UNAVAILABLE',
|
|
65
|
+
'PLAN_RECOVERY_REQUIRED',
|
|
66
|
+
'BACKUP_FAILED',
|
|
67
|
+
'OUTPUT_SCHEMA_DRIFT',
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
function parseJson(value, fallback = null) {
|
|
71
|
+
try { return value == null ? fallback : JSON.parse(value); }
|
|
72
|
+
catch { return fallback; }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function errorCode(error) {
|
|
76
|
+
return String(error?.code || error?.name || 'CHECKPOINT_REJECTED');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isHardError(error) {
|
|
80
|
+
const code = errorCode(error);
|
|
81
|
+
return HARD_ERROR_CODES.has(code)
|
|
82
|
+
|| code.startsWith('PATH_')
|
|
83
|
+
|| code.startsWith('STATE_CORRUPT')
|
|
84
|
+
|| code.startsWith('SQLITE_');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function priorFacadeResult(db, idempotencyKey, operation = null) {
|
|
88
|
+
if (!idempotencyKey) return null;
|
|
89
|
+
const rows = db.prepare(
|
|
90
|
+
`SELECT payload_json FROM events
|
|
91
|
+
WHERE type = 'ultra_facade_call'
|
|
92
|
+
ORDER BY id DESC LIMIT 2000`,
|
|
93
|
+
).all();
|
|
94
|
+
for (const row of rows) {
|
|
95
|
+
const payload = parseJson(row.payload_json, {});
|
|
96
|
+
if (payload.idempotency_key === idempotencyKey
|
|
97
|
+
&& (operation === null || payload.operation === operation)
|
|
98
|
+
&& payload.accepted === true) {
|
|
99
|
+
return payload.result;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function rememberFacadeResult(db, {
|
|
106
|
+
idempotencyKey,
|
|
107
|
+
operation,
|
|
108
|
+
result,
|
|
109
|
+
changeId = null,
|
|
110
|
+
taskId = null,
|
|
111
|
+
}) {
|
|
112
|
+
ops.appendEvent(db, {
|
|
113
|
+
type: 'ultra_facade_call',
|
|
114
|
+
change_id: changeId,
|
|
115
|
+
task_id: taskId,
|
|
116
|
+
payload: {
|
|
117
|
+
idempotency_key: idempotencyKey,
|
|
118
|
+
operation,
|
|
119
|
+
accepted: true,
|
|
120
|
+
result,
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function currentBaselineId(db) {
|
|
126
|
+
return db.prepare(
|
|
127
|
+
`SELECT id FROM baselines
|
|
128
|
+
WHERE status <> 'superseded'
|
|
129
|
+
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
130
|
+
).get()?.id || null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function currentWorkflow(db, stage, scope = {}, rootDir) {
|
|
134
|
+
return workflows.listWorkflows(db, {
|
|
135
|
+
kind: stage,
|
|
136
|
+
change_id: scope.change_id || null,
|
|
137
|
+
task_id: scope.task_id || null,
|
|
138
|
+
limit: 100,
|
|
139
|
+
}, { rootDir }).find((run) => ['active', 'blocked', 'ready'].includes(run.status)) || null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function ensureWorkflow(db, stage, scope = {}, payload = {}, rootDir) {
|
|
143
|
+
const existing = currentWorkflow(db, stage, scope, rootDir);
|
|
144
|
+
if (existing) return existing;
|
|
145
|
+
const input = {
|
|
146
|
+
kind: stage,
|
|
147
|
+
subject: typeof payload.subject === 'string' && payload.subject.trim()
|
|
148
|
+
? payload.subject.trim()
|
|
149
|
+
: `Record the ${stage} stage for ${scope.task_id || scope.change_id || 'this project'}.`,
|
|
150
|
+
baseline_id: currentBaselineId(db),
|
|
151
|
+
change_id: scope.change_id || undefined,
|
|
152
|
+
task_id: scope.task_id || undefined,
|
|
153
|
+
metadata: { public_facade: true },
|
|
154
|
+
};
|
|
155
|
+
if (stage === 'research') {
|
|
156
|
+
input.mode = payload.mode || 'custom';
|
|
157
|
+
if (payload.coverage !== undefined) input.coverage = payload.coverage;
|
|
158
|
+
if (payload.selected_steps !== undefined) input.selected_steps = payload.selected_steps;
|
|
159
|
+
}
|
|
160
|
+
return workflows.startWorkflow(db, input, { rootDir });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function contextOutput(compiled, rootDir) {
|
|
164
|
+
return {
|
|
165
|
+
path: path.relative(rootDir, compiled.context_manifest_path).split(path.sep).join('/'),
|
|
166
|
+
kind: 'context-manifest',
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function compileStageContext(db, stage, scope, input, rootDir) {
|
|
171
|
+
const expectation = STAGE_CONTEXT[stage];
|
|
172
|
+
if (!expectation || !scope.change_id) return null;
|
|
173
|
+
return changes.compileContext(db, {
|
|
174
|
+
id: scope.change_id,
|
|
175
|
+
task_id: scope.task_id,
|
|
176
|
+
role: expectation.role,
|
|
177
|
+
gate: expectation.gate,
|
|
178
|
+
...(Array.isArray(input.context_refs) ? { context_refs: input.context_refs } : {}),
|
|
179
|
+
...(input.budget && typeof input.budget === 'object' ? { budget: input.budget } : {}),
|
|
180
|
+
}, { rootDir });
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function readContext(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
184
|
+
const stage = input.stage || 'project';
|
|
185
|
+
const scope = input.scope || {};
|
|
186
|
+
const health = doctor.inspectSystem(db, { rootDir });
|
|
187
|
+
const baseline = baselines.inspectBaseline(db, { rootDir });
|
|
188
|
+
const project = {
|
|
189
|
+
health: health.status,
|
|
190
|
+
baseline,
|
|
191
|
+
changes: changes.listChanges(db, { limit: input.detail === 'full' ? 500 : 20 }),
|
|
192
|
+
tasks: ops.listTasks(db, { status: 'any', limit: input.detail === 'full' ? 1000 : 100 }),
|
|
193
|
+
workflows: workflows.listWorkflows(
|
|
194
|
+
db,
|
|
195
|
+
{ limit: input.detail === 'full' ? 500 : 50 },
|
|
196
|
+
{ rootDir },
|
|
197
|
+
),
|
|
198
|
+
team_checkpoint: taskLedger.inspectTaskLedger(db, { rootDir }),
|
|
199
|
+
};
|
|
200
|
+
const expectation = STAGE_CONTEXT[stage];
|
|
201
|
+
const snapshot = expectation && scope.change_id
|
|
202
|
+
? db.prepare(
|
|
203
|
+
`SELECT * FROM context_snapshots
|
|
204
|
+
WHERE change_id = ? AND (? IS NULL OR task_id = ?)
|
|
205
|
+
AND role = ? AND gate = ?
|
|
206
|
+
ORDER BY created_at DESC, rowid DESC LIMIT 1`,
|
|
207
|
+
).get(
|
|
208
|
+
scope.change_id,
|
|
209
|
+
scope.task_id || null,
|
|
210
|
+
scope.task_id || null,
|
|
211
|
+
expectation.role,
|
|
212
|
+
expectation.gate,
|
|
213
|
+
)
|
|
214
|
+
: null;
|
|
215
|
+
const context = snapshot
|
|
216
|
+
? {
|
|
217
|
+
snapshot_id: snapshot.id,
|
|
218
|
+
manifest_path: snapshot.manifest_path,
|
|
219
|
+
manifest_hash: snapshot.manifest_hash,
|
|
220
|
+
readiness: snapshot.readiness,
|
|
221
|
+
blockers: parseJson(snapshot.blockers_json, []),
|
|
222
|
+
spine: parseJson(snapshot.context_json, {}),
|
|
223
|
+
}
|
|
224
|
+
: null;
|
|
225
|
+
return {
|
|
226
|
+
status: context?.readiness
|
|
227
|
+
|| (health.status === 'healthy' ? 'ready' : 'needs_attention'),
|
|
228
|
+
warnings: baseline.warnings || [],
|
|
229
|
+
blockers: [...new Set([
|
|
230
|
+
...(baseline.blockers || []),
|
|
231
|
+
...(context?.blockers || []),
|
|
232
|
+
])],
|
|
233
|
+
project,
|
|
234
|
+
workflow: expectation ? currentWorkflow(db, stage, scope, rootDir) : null,
|
|
235
|
+
context,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function record(db, input = {}, {
|
|
240
|
+
rootDir = process.cwd(),
|
|
241
|
+
callLegacy,
|
|
242
|
+
} = {}) {
|
|
243
|
+
const results = [];
|
|
244
|
+
for (const entry of input.entries || []) {
|
|
245
|
+
const cached = db
|
|
246
|
+
? priorFacadeResult(db, entry.idempotency_key, entry.operation)
|
|
247
|
+
: null;
|
|
248
|
+
if (cached) {
|
|
249
|
+
results.push({ operation: entry.operation, accepted: true, idempotent: true, result: cached });
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (entry.operation === 'workflow.abandon') {
|
|
253
|
+
try {
|
|
254
|
+
const result = workflows.abandonWorkflow(db, entry.data, { rootDir });
|
|
255
|
+
rememberFacadeResult(db, {
|
|
256
|
+
idempotencyKey: entry.idempotency_key,
|
|
257
|
+
operation: entry.operation,
|
|
258
|
+
result,
|
|
259
|
+
changeId: result.change_id,
|
|
260
|
+
taskId: result.task_id,
|
|
261
|
+
});
|
|
262
|
+
results.push({ operation: entry.operation, accepted: true, idempotent: false, result });
|
|
263
|
+
} catch (error) {
|
|
264
|
+
if (isHardError(error)) throw error;
|
|
265
|
+
results.push({
|
|
266
|
+
operation: entry.operation,
|
|
267
|
+
accepted: false,
|
|
268
|
+
mutable: true,
|
|
269
|
+
blockers: [errorCode(error)],
|
|
270
|
+
diagnostic: { code: errorCode(error), message: error.message },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (entry.operation === 'event.append') {
|
|
276
|
+
const event = ops.appendEvent(db, entry.data);
|
|
277
|
+
rememberFacadeResult(db, {
|
|
278
|
+
idempotencyKey: entry.idempotency_key,
|
|
279
|
+
operation: entry.operation,
|
|
280
|
+
result: event,
|
|
281
|
+
changeId: entry.data.change_id || null,
|
|
282
|
+
taskId: entry.data.task_id || null,
|
|
283
|
+
});
|
|
284
|
+
results.push({ operation: entry.operation, accepted: true, idempotent: false, result: event });
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
if (!RECORD_OPERATIONS.has(entry.operation)) {
|
|
288
|
+
results.push({
|
|
289
|
+
operation: entry.operation,
|
|
290
|
+
accepted: false,
|
|
291
|
+
mutable: true,
|
|
292
|
+
blockers: ['ULTRA_RECORD_OPERATION_UNSUPPORTED'],
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
try {
|
|
297
|
+
const result = await callLegacy(entry.operation, entry.data);
|
|
298
|
+
if (db) {
|
|
299
|
+
rememberFacadeResult(db, {
|
|
300
|
+
idempotencyKey: entry.idempotency_key,
|
|
301
|
+
operation: entry.operation,
|
|
302
|
+
result,
|
|
303
|
+
changeId: entry.data.change_id || entry.data.id || null,
|
|
304
|
+
taskId: entry.data.task_id || null,
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
results.push({ operation: entry.operation, accepted: true, idempotent: false, result });
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (isHardError(error)) throw error;
|
|
310
|
+
results.push({
|
|
311
|
+
operation: entry.operation,
|
|
312
|
+
accepted: false,
|
|
313
|
+
mutable: true,
|
|
314
|
+
blockers: [errorCode(error)],
|
|
315
|
+
diagnostic: { code: errorCode(error), message: error.message, details: error.details },
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return {
|
|
320
|
+
accepted: results.every((item) => item.accepted),
|
|
321
|
+
mutable: true,
|
|
322
|
+
results,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function addStepOutput(steps, stepId, output) {
|
|
327
|
+
const current = steps.find((step) => step.step_id === stepId);
|
|
328
|
+
if (current) current.outputs = [...(current.outputs || []), output];
|
|
329
|
+
else steps.push({ step_id: stepId, outputs: [output] });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function checkpointOutputStep(stage, output) {
|
|
333
|
+
if (output.step_id) return output.step_id;
|
|
334
|
+
if (stage === 'test') return 'write-report';
|
|
335
|
+
if (stage === 'deliver') return 'verify-delivery';
|
|
336
|
+
if (stage === 'review') {
|
|
337
|
+
if (output.role === 'spec_review') return 'review-specification';
|
|
338
|
+
if (output.role === 'engineering_review') return 'review-engineering';
|
|
339
|
+
return 'coordinate-findings';
|
|
340
|
+
}
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function checkpoint(db, input = {}, {
|
|
345
|
+
rootDir = process.cwd(),
|
|
346
|
+
callLegacy,
|
|
347
|
+
} = {}) {
|
|
348
|
+
const cached = priorFacadeResult(db, input.idempotency_key, `checkpoint:${input.stage}`);
|
|
349
|
+
if (cached) return { ...cached, idempotent: true };
|
|
350
|
+
const stage = input.stage;
|
|
351
|
+
const scope = input.scope || {};
|
|
352
|
+
const payload = input.payload || {};
|
|
353
|
+
let run = null;
|
|
354
|
+
try {
|
|
355
|
+
if (stage === 'plan') {
|
|
356
|
+
const taskCount = ops.listTasks(db, { change_id: scope.change_id }).length;
|
|
357
|
+
if (taskCount === 0) {
|
|
358
|
+
return {
|
|
359
|
+
accepted: false,
|
|
360
|
+
mutable: true,
|
|
361
|
+
blockers: ['WORKFLOW_TASKS_REQUIRED'],
|
|
362
|
+
workflow: null,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
run = ensureWorkflow(db, stage, scope, payload, rootDir);
|
|
367
|
+
const steps = Array.isArray(payload.steps)
|
|
368
|
+
? payload.steps.map((step) => ({ ...step }))
|
|
369
|
+
: [];
|
|
370
|
+
const compiled = STAGE_CONTEXT[stage]
|
|
371
|
+
? compileStageContext(db, stage, scope, payload, rootDir)
|
|
372
|
+
: null;
|
|
373
|
+
if (compiled) {
|
|
374
|
+
addStepOutput(
|
|
375
|
+
steps,
|
|
376
|
+
stage === 'deliver' ? 'verify-candidate' : 'compile-context',
|
|
377
|
+
contextOutput(compiled, rootDir),
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
let stageResult = null;
|
|
381
|
+
if (stage === 'plan') {
|
|
382
|
+
stageResult = await callLegacy('plan.export', { change_id: scope.change_id });
|
|
383
|
+
addStepOutput(steps, 'verify-plan', {
|
|
384
|
+
path: path.relative(rootDir, stageResult.plan_path).split(path.sep).join('/'),
|
|
385
|
+
kind: 'execution-plan',
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
for (const output of Array.isArray(payload.outputs) ? payload.outputs : []) {
|
|
389
|
+
const stepId = checkpointOutputStep(stage, output);
|
|
390
|
+
if (!stepId) continue;
|
|
391
|
+
const { step_id: _stepId, role: _role, ...normalized } = output;
|
|
392
|
+
addStepOutput(steps, stepId, normalized);
|
|
393
|
+
}
|
|
394
|
+
const prepared = workflows.prepareWorkflowCheckpoint(db, {
|
|
395
|
+
id: run.id,
|
|
396
|
+
steps,
|
|
397
|
+
evidence: payload.evidence,
|
|
398
|
+
}, { rootDir });
|
|
399
|
+
if (!prepared.ready) {
|
|
400
|
+
return {
|
|
401
|
+
accepted: false,
|
|
402
|
+
mutable: true,
|
|
403
|
+
blockers: prepared.blockers,
|
|
404
|
+
workflow: prepared.workflow,
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const completed = workflows.completeWorkflow(db, {
|
|
408
|
+
id: run.id,
|
|
409
|
+
...(payload.approval ? { approval: payload.approval } : {}),
|
|
410
|
+
}, { rootDir });
|
|
411
|
+
if (stage === 'plan') {
|
|
412
|
+
stageResult = {
|
|
413
|
+
...stageResult,
|
|
414
|
+
team_checkpoint: taskLedger.publishTaskLedger(db, {
|
|
415
|
+
rootDir,
|
|
416
|
+
reason: 'plan_accepted',
|
|
417
|
+
}),
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
const result = {
|
|
421
|
+
accepted: true,
|
|
422
|
+
mutable: false,
|
|
423
|
+
blockers: [],
|
|
424
|
+
workflow: completed,
|
|
425
|
+
result: stageResult,
|
|
426
|
+
idempotent: false,
|
|
427
|
+
};
|
|
428
|
+
rememberFacadeResult(db, {
|
|
429
|
+
idempotencyKey: input.idempotency_key,
|
|
430
|
+
operation: `checkpoint:${stage}`,
|
|
431
|
+
result,
|
|
432
|
+
changeId: scope.change_id || null,
|
|
433
|
+
taskId: scope.task_id || null,
|
|
434
|
+
});
|
|
435
|
+
ops.appendEvent(db, {
|
|
436
|
+
type: 'ultra_checkpoint_accepted',
|
|
437
|
+
change_id: scope.change_id || null,
|
|
438
|
+
task_id: scope.task_id || null,
|
|
439
|
+
payload: { stage, workflow_id: completed.id, idempotency_key: input.idempotency_key },
|
|
440
|
+
});
|
|
441
|
+
return result;
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (isHardError(error)) throw error;
|
|
444
|
+
if (run?.id) {
|
|
445
|
+
workflows.reopenWorkflowDraft(db, {
|
|
446
|
+
id: run.id,
|
|
447
|
+
blocker: errorCode(error),
|
|
448
|
+
}, { rootDir });
|
|
449
|
+
}
|
|
450
|
+
ops.appendEvent(db, {
|
|
451
|
+
type: 'ultra_checkpoint_rejected',
|
|
452
|
+
change_id: scope.change_id || null,
|
|
453
|
+
task_id: scope.task_id || null,
|
|
454
|
+
payload: {
|
|
455
|
+
stage,
|
|
456
|
+
workflow_id: run?.id || null,
|
|
457
|
+
code: errorCode(error),
|
|
458
|
+
idempotency_key: input.idempotency_key,
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
return {
|
|
462
|
+
accepted: false,
|
|
463
|
+
mutable: true,
|
|
464
|
+
blockers: [errorCode(error)],
|
|
465
|
+
workflow: run?.id ? workflows.readWorkflow(db, run.id, { rootDir }) : null,
|
|
466
|
+
diagnostic: { code: errorCode(error), message: error.message, details: error.details },
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function sync(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
472
|
+
if (input.action === 'inspect') return taskLedger.inspectTaskLedger(db, { rootDir });
|
|
473
|
+
if (input.action === 'import') return taskLedger.importTaskLedger(db, { rootDir });
|
|
474
|
+
const cached = priorFacadeResult(db, input.idempotency_key, 'sync:publish');
|
|
475
|
+
if (cached) return { ...cached, idempotent: true };
|
|
476
|
+
const result = taskLedger.publishTaskLedger(db, {
|
|
477
|
+
rootDir,
|
|
478
|
+
reason: input.reason || 'manual_checkpoint',
|
|
479
|
+
});
|
|
480
|
+
if (input.idempotency_key) {
|
|
481
|
+
rememberFacadeResult(db, {
|
|
482
|
+
idempotencyKey: input.idempotency_key,
|
|
483
|
+
operation: 'sync:publish',
|
|
484
|
+
result,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
async function session(db, input = {}, { callLegacy } = {}) {
|
|
491
|
+
const scope = input.scope || {};
|
|
492
|
+
const payload = input.payload || {};
|
|
493
|
+
const operation = {
|
|
494
|
+
admission: 'session.admission_check',
|
|
495
|
+
acquire: 'session.spawn',
|
|
496
|
+
get: 'session.get',
|
|
497
|
+
list: 'session.list',
|
|
498
|
+
heartbeat: 'session.heartbeat',
|
|
499
|
+
release: 'session.close',
|
|
500
|
+
}[input.action];
|
|
501
|
+
const args = {
|
|
502
|
+
...payload,
|
|
503
|
+
...(scope.task_id ? { task_id: scope.task_id } : {}),
|
|
504
|
+
...(scope.sid ? { sid: scope.sid } : {}),
|
|
505
|
+
};
|
|
506
|
+
if (input.action === 'release' && args.status === undefined) args.status = 'completed';
|
|
507
|
+
return callLegacy(operation, args);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function archive(db, input = {}, {
|
|
511
|
+
rootDir = process.cwd(),
|
|
512
|
+
callLegacy,
|
|
513
|
+
} = {}) {
|
|
514
|
+
const cached = priorFacadeResult(db, input.idempotency_key, 'archive');
|
|
515
|
+
if (cached) return { ...cached, idempotent: true };
|
|
516
|
+
const payload = input.payload || {};
|
|
517
|
+
let run = null;
|
|
518
|
+
try {
|
|
519
|
+
run = ensureWorkflow(
|
|
520
|
+
db,
|
|
521
|
+
'deliver',
|
|
522
|
+
{ change_id: input.change_id },
|
|
523
|
+
payload,
|
|
524
|
+
rootDir,
|
|
525
|
+
);
|
|
526
|
+
const steps = Array.isArray(payload.steps)
|
|
527
|
+
? payload.steps.map((step) => ({ ...step }))
|
|
528
|
+
: [];
|
|
529
|
+
const compiled = compileStageContext(
|
|
530
|
+
db,
|
|
531
|
+
'deliver',
|
|
532
|
+
{ change_id: input.change_id },
|
|
533
|
+
payload,
|
|
534
|
+
rootDir,
|
|
535
|
+
);
|
|
536
|
+
if (compiled) {
|
|
537
|
+
addStepOutput(steps, 'verify-candidate', contextOutput(compiled, rootDir));
|
|
538
|
+
}
|
|
539
|
+
for (const output of Array.isArray(payload.outputs) ? payload.outputs : []) {
|
|
540
|
+
const stepId = checkpointOutputStep('deliver', output);
|
|
541
|
+
if (!stepId) continue;
|
|
542
|
+
const { step_id: _stepId, role: _role, ...normalized } = output;
|
|
543
|
+
addStepOutput(steps, stepId, normalized);
|
|
544
|
+
}
|
|
545
|
+
const prepared = workflows.prepareWorkflowCheckpoint(db, {
|
|
546
|
+
id: run.id,
|
|
547
|
+
steps,
|
|
548
|
+
evidence: payload.evidence,
|
|
549
|
+
}, { rootDir });
|
|
550
|
+
if (!prepared.ready) {
|
|
551
|
+
return {
|
|
552
|
+
accepted: false,
|
|
553
|
+
mutable: true,
|
|
554
|
+
blockers: prepared.blockers,
|
|
555
|
+
workflow: prepared.workflow,
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
const convergence = await callLegacy('change.converge', { id: input.change_id });
|
|
559
|
+
if (convergence.ready !== true) {
|
|
560
|
+
workflows.reopenWorkflowDraft(db, {
|
|
561
|
+
id: run.id,
|
|
562
|
+
blocker: 'CHANGE_NOT_READY',
|
|
563
|
+
}, { rootDir });
|
|
564
|
+
return {
|
|
565
|
+
accepted: false,
|
|
566
|
+
mutable: true,
|
|
567
|
+
blockers: convergence.blockers || ['CHANGE_NOT_READY'],
|
|
568
|
+
convergence,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
const {
|
|
572
|
+
steps: _steps,
|
|
573
|
+
outputs: _outputs,
|
|
574
|
+
evidence: _evidence,
|
|
575
|
+
context_refs: _contextRefs,
|
|
576
|
+
budget: _budget,
|
|
577
|
+
...archivePayload
|
|
578
|
+
} = payload;
|
|
579
|
+
const result = await callLegacy('change.archive', {
|
|
580
|
+
id: input.change_id,
|
|
581
|
+
...archivePayload,
|
|
582
|
+
});
|
|
583
|
+
const completed = workflows.completeWorkflow(db, { id: run.id }, { rootDir });
|
|
584
|
+
const output = {
|
|
585
|
+
accepted: true,
|
|
586
|
+
mutable: false,
|
|
587
|
+
blockers: [],
|
|
588
|
+
workflow: completed,
|
|
589
|
+
result,
|
|
590
|
+
idempotent: false,
|
|
591
|
+
};
|
|
592
|
+
rememberFacadeResult(db, {
|
|
593
|
+
idempotencyKey: input.idempotency_key,
|
|
594
|
+
operation: 'archive',
|
|
595
|
+
result: output,
|
|
596
|
+
changeId: input.change_id,
|
|
597
|
+
});
|
|
598
|
+
return output;
|
|
599
|
+
} catch (error) {
|
|
600
|
+
if (isHardError(error)) throw error;
|
|
601
|
+
if (run?.id) {
|
|
602
|
+
workflows.reopenWorkflowDraft(db, {
|
|
603
|
+
id: run.id,
|
|
604
|
+
blocker: errorCode(error),
|
|
605
|
+
}, { rootDir });
|
|
606
|
+
}
|
|
607
|
+
return {
|
|
608
|
+
accepted: false,
|
|
609
|
+
mutable: true,
|
|
610
|
+
blockers: [errorCode(error)],
|
|
611
|
+
diagnostic: { code: errorCode(error), message: error.message, details: error.details },
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function dispatch(name, input, db, context = {}) {
|
|
617
|
+
if (name === 'ultra.context') return readContext(db, input, context);
|
|
618
|
+
if (name === 'ultra.record') return record(db, input, context);
|
|
619
|
+
if (name === 'ultra.checkpoint') return checkpoint(db, input, context);
|
|
620
|
+
if (name === 'ultra.sync') return sync(db, input, context);
|
|
621
|
+
if (name === 'ultra.session') return session(db, input, context);
|
|
622
|
+
if (name === 'ultra.archive') return archive(db, input, context);
|
|
623
|
+
if (name === 'ultra.doctor') {
|
|
624
|
+
return doctor.runDoctor(db, {
|
|
625
|
+
rootDir: context.rootDir || process.cwd(),
|
|
626
|
+
repair: input.repair === true,
|
|
627
|
+
project: context.projector,
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
const error = new Error(`unhandled public tool ${name}`);
|
|
631
|
+
error.code = 'UNKNOWN_TOOL';
|
|
632
|
+
throw error;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
module.exports = {
|
|
636
|
+
PUBLIC_TOOLS,
|
|
637
|
+
RECORD_OPERATIONS,
|
|
638
|
+
dispatch,
|
|
639
|
+
readContext,
|
|
640
|
+
record,
|
|
641
|
+
checkpoint,
|
|
642
|
+
sync,
|
|
643
|
+
session,
|
|
644
|
+
archive,
|
|
645
|
+
isHardError,
|
|
646
|
+
};
|