ultra-builder-pro-cli 0.24.0 → 0.24.2
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/tasks/tasks.json +4 -2
- package/CHANGELOG.md +48 -0
- package/README.md +48 -25
- package/agents/code-reviewer.md +4 -5
- package/agents/debugger.md +8 -8
- package/docs/AGENT-CONTEXT.md +22 -8
- package/docs/ARCHITECTURE.md +30 -9
- package/docs/ARTIFACT-AUTHORITY.md +10 -7
- package/docs/LEGACY-CLI-CROSSWALK.md +1 -1
- package/docs/STATE-DB-ACCESS-POLICY.md +8 -1
- package/docs/WORKFLOW-LIFECYCLE.md +38 -23
- package/hooks/health_check.py +11 -6
- package/mcp-server/lib/artifact-registry.cjs +7 -56
- package/mcp-server/lib/baseline-workflow.cjs +74 -46
- package/mcp-server/lib/change-authority.cjs +26 -0
- package/mcp-server/lib/change-packet.cjs +4 -32
- package/mcp-server/lib/change-workflow.cjs +33 -2724
- package/mcp-server/lib/legacy-change-workflow.cjs +2809 -0
- package/mcp-server/lib/state-ops.cjs +21 -3
- package/mcp-server/lib/task-expander.cjs +6 -1
- package/mcp-server/lib/ultra-facade.cjs +280 -47
- package/mcp-server/server.cjs +1 -1
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +2 -1
- package/skills/ultra-deliver/SKILL.md +15 -17
- package/skills/ultra-dev/SKILL.md +3 -1
- package/skills/ultra-doctor/SKILL.md +2 -2
- package/skills/ultra-init/references/brownfield-adoption.md +13 -10
- package/skills/ultra-plan/SKILL.md +10 -8
- package/skills/ultra-research/SKILL.md +8 -4
- package/skills/ultra-review/SKILL.md +6 -4
- package/skills/ultra-test/SKILL.md +6 -4
- package/skills/ultra-think/SKILL.md +3 -2
- package/spec/mcp-tools.yaml +6 -2
|
@@ -21,7 +21,7 @@ const STATUS_TRANSITIONS = Object.freeze({
|
|
|
21
21
|
in_progress: new Set(['completed', 'blocked', 'pending']),
|
|
22
22
|
blocked: new Set(['pending', 'in_progress']),
|
|
23
23
|
expanded: new Set(['completed']),
|
|
24
|
-
completed: new Set(),
|
|
24
|
+
completed: new Set(['in_progress']),
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
const TASK_FIELDS = Object.freeze([
|
|
@@ -529,6 +529,16 @@ function patchTask(db, id, patch = {}) {
|
|
|
529
529
|
`cannot transition task ${id} from ${current.status} to ${nextStatus}`,
|
|
530
530
|
);
|
|
531
531
|
}
|
|
532
|
+
if (current.status === 'completed' && nextStatus === 'in_progress') {
|
|
533
|
+
if (!Object.prototype.hasOwnProperty.call(patch, 'completion_commit')) {
|
|
534
|
+
sets.push('completion_commit = ?');
|
|
535
|
+
params.push(null);
|
|
536
|
+
}
|
|
537
|
+
if (!Object.prototype.hasOwnProperty.call(patch, 'session_id')) {
|
|
538
|
+
sets.push('session_id = ?');
|
|
539
|
+
params.push(null);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
532
542
|
sets.push('status = ?');
|
|
533
543
|
params.push(nextStatus);
|
|
534
544
|
}
|
|
@@ -542,10 +552,18 @@ function patchTask(db, id, patch = {}) {
|
|
|
542
552
|
|
|
543
553
|
if (nextStatus && nextStatus !== current.status) {
|
|
544
554
|
appendEventInTx(db, {
|
|
545
|
-
type:
|
|
555
|
+
type: current.status === 'completed' && nextStatus === 'in_progress'
|
|
556
|
+
? 'task_reopened'
|
|
557
|
+
: statusEventType(nextStatus),
|
|
546
558
|
task_id: id,
|
|
547
559
|
change_id: patch.change_id !== undefined ? patch.change_id : current.change_id,
|
|
548
|
-
payload: {
|
|
560
|
+
payload: {
|
|
561
|
+
from: current.status,
|
|
562
|
+
to: nextStatus,
|
|
563
|
+
...(current.status === 'completed' && nextStatus === 'in_progress'
|
|
564
|
+
? { previous_completion_commit: current.completion_commit }
|
|
565
|
+
: {}),
|
|
566
|
+
},
|
|
549
567
|
});
|
|
550
568
|
}
|
|
551
569
|
const contractFields = Object.keys(patch).filter((field) => TASK_CONTRACT_PATCH_FIELDS.has(field));
|
|
@@ -81,7 +81,11 @@ function expandTask(db, {
|
|
|
81
81
|
});
|
|
82
82
|
|
|
83
83
|
return ops.tx(db, () => {
|
|
84
|
-
changes.assertTaskCreationAllowed(
|
|
84
|
+
const admission = changes.assertTaskCreationAllowed(
|
|
85
|
+
db,
|
|
86
|
+
{ change_id: parent.change_id },
|
|
87
|
+
{ rootDir },
|
|
88
|
+
);
|
|
85
89
|
for (const child of normalized) {
|
|
86
90
|
ops.createTask(db, child);
|
|
87
91
|
}
|
|
@@ -89,6 +93,7 @@ function expandTask(db, {
|
|
|
89
93
|
return {
|
|
90
94
|
parent_id: parent.id,
|
|
91
95
|
children: normalized,
|
|
96
|
+
diagnostics: admission.diagnostics,
|
|
92
97
|
};
|
|
93
98
|
});
|
|
94
99
|
}
|
|
@@ -45,6 +45,10 @@ const HARD_ERROR_CODES = new Set([
|
|
|
45
45
|
'ARCHIVE_RUNTIME_UNAVAILABLE',
|
|
46
46
|
'BACKUP_FAILED',
|
|
47
47
|
'CHECKPOINT_DIGEST_MISMATCH',
|
|
48
|
+
'CHECKPOINT_EVIDENCE_AUTHORITY_MISSING',
|
|
49
|
+
'CHECKPOINT_EVIDENCE_DIGEST_MISMATCH',
|
|
50
|
+
'CHECKPOINT_SCOPE_MISMATCH',
|
|
51
|
+
'CHECKPOINT_SCOPE_NOT_FOUND',
|
|
48
52
|
'CONTEXT_ENVELOPE_LIMIT_EXCEEDED',
|
|
49
53
|
'CONTEXT_ENVELOPE_DIGEST_MISMATCH',
|
|
50
54
|
'CONTEXT_ENVELOPE_FILE_DRIFT',
|
|
@@ -131,6 +135,48 @@ function rememberResult(db, {
|
|
|
131
135
|
});
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
function rememberRejectedAttempt(db, tool, input, result) {
|
|
139
|
+
if (!db || result?.accepted !== false) return result;
|
|
140
|
+
const nested = Array.isArray(result.results)
|
|
141
|
+
? result.results.flatMap((item) => item.diagnostics || [])
|
|
142
|
+
: [];
|
|
143
|
+
const diagnostics = [...(result.diagnostics || []), ...nested]
|
|
144
|
+
.map((item) => ({
|
|
145
|
+
code: item.code || 'NEEDS_ATTENTION',
|
|
146
|
+
severity: item.severity || 'needs_attention',
|
|
147
|
+
message: item.message || null,
|
|
148
|
+
...(item.details === undefined ? {} : { details: item.details }),
|
|
149
|
+
}));
|
|
150
|
+
const entries = Array.isArray(input.entries) ? input.entries : [];
|
|
151
|
+
const changeIds = [...new Set(entries.flatMap((entry) => [
|
|
152
|
+
entry.data?.change_id,
|
|
153
|
+
entry.kind === 'change_contract' ? entry.data?.id : null,
|
|
154
|
+
]).filter(Boolean))];
|
|
155
|
+
const taskIds = [...new Set(entries.flatMap((entry) => [
|
|
156
|
+
entry.kind.startsWith('task_') ? entry.data?.id : null,
|
|
157
|
+
input.scope?.task_id,
|
|
158
|
+
]).filter(Boolean))];
|
|
159
|
+
const scopeChangeId = input.scope?.change_id || input.change_id || null;
|
|
160
|
+
ops.appendEvent(db, {
|
|
161
|
+
type: 'ultra_kernel_attempt',
|
|
162
|
+
change_id: changeIds.length === 1 ? changeIds[0] : scopeChangeId,
|
|
163
|
+
task_id: taskIds.length === 1 ? taskIds[0] : null,
|
|
164
|
+
payload: {
|
|
165
|
+
tool,
|
|
166
|
+
accepted: false,
|
|
167
|
+
mutable: result.mutable !== false,
|
|
168
|
+
operations: entries.map((entry) => `${entry.kind}:${entry.action}`),
|
|
169
|
+
idempotency_keys: entries.length > 0
|
|
170
|
+
? entries.map((entry) => entry.idempotency_key).filter(Boolean)
|
|
171
|
+
: [input.idempotency_key].filter(Boolean),
|
|
172
|
+
scope: input.scope || (input.change_id ? { change_id: input.change_id } : {}),
|
|
173
|
+
blockers: result.blockers || [],
|
|
174
|
+
diagnostics,
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
|
|
134
180
|
function diagnostic(error, severity = 'needs_attention') {
|
|
135
181
|
return {
|
|
136
182
|
code: errorCode(error),
|
|
@@ -140,17 +186,79 @@ function diagnostic(error, severity = 'needs_attention') {
|
|
|
140
186
|
};
|
|
141
187
|
}
|
|
142
188
|
|
|
189
|
+
function rejectedAttemptAudit(db) {
|
|
190
|
+
return db.prepare(
|
|
191
|
+
`SELECT id, ts, task_id, change_id, payload_json
|
|
192
|
+
FROM events
|
|
193
|
+
WHERE type = 'ultra_kernel_attempt'
|
|
194
|
+
ORDER BY id DESC
|
|
195
|
+
LIMIT 5`,
|
|
196
|
+
).all().map((row) => {
|
|
197
|
+
const payload = parseJson(row.payload_json, {});
|
|
198
|
+
return {
|
|
199
|
+
event_id: row.id,
|
|
200
|
+
recorded_at: row.ts,
|
|
201
|
+
tool: payload.tool || null,
|
|
202
|
+
task_id: row.task_id || null,
|
|
203
|
+
change_id: row.change_id || null,
|
|
204
|
+
operations: (payload.operations || []).slice(0, 12),
|
|
205
|
+
blockers: (payload.blockers || []).slice(0, 12),
|
|
206
|
+
diagnostics: (payload.diagnostics || []).slice(0, 8).map((item) => ({
|
|
207
|
+
code: item.code || 'NEEDS_ATTENTION',
|
|
208
|
+
severity: item.severity || 'needs_attention',
|
|
209
|
+
message: typeof item.message === 'string'
|
|
210
|
+
? item.message.slice(0, 500)
|
|
211
|
+
: null,
|
|
212
|
+
})),
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
143
217
|
function selectedScope(db, input = {}) {
|
|
144
218
|
const scope = input.scope || {};
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
219
|
+
const task = scope.task_id ? ops.readTask(db, scope.task_id) : null;
|
|
220
|
+
if (scope.task_id && !task) {
|
|
221
|
+
throw Object.assign(
|
|
222
|
+
new Error(`checkpoint task scope does not exist: ${scope.task_id}`),
|
|
223
|
+
{
|
|
224
|
+
code: 'CHECKPOINT_SCOPE_NOT_FOUND',
|
|
225
|
+
details: { scope_type: 'task', scope_id: scope.task_id },
|
|
226
|
+
},
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const changeId = scope.change_id || task?.change_id || null;
|
|
230
|
+
const change = changeId ? changes.readChange(db, changeId) : null;
|
|
231
|
+
if (changeId && !change) {
|
|
232
|
+
throw Object.assign(
|
|
233
|
+
new Error(`checkpoint Change scope does not exist: ${changeId}`),
|
|
234
|
+
{
|
|
235
|
+
code: 'CHECKPOINT_SCOPE_NOT_FOUND',
|
|
236
|
+
details: { scope_type: 'change', scope_id: changeId },
|
|
237
|
+
},
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if (task && scope.change_id && task.change_id !== scope.change_id) {
|
|
241
|
+
throw Object.assign(
|
|
242
|
+
new Error(
|
|
243
|
+
`checkpoint task ${task.id} belongs to Change ${task.change_id || '(none)'}, not ${scope.change_id}`,
|
|
244
|
+
),
|
|
245
|
+
{
|
|
246
|
+
code: 'CHECKPOINT_SCOPE_MISMATCH',
|
|
247
|
+
details: {
|
|
248
|
+
task_id: task.id,
|
|
249
|
+
task_change_id: task.change_id || null,
|
|
250
|
+
requested_change_id: scope.change_id,
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
if (task) {
|
|
148
256
|
return {
|
|
149
|
-
change_id:
|
|
257
|
+
...(changeId ? { change_id: changeId } : {}),
|
|
150
258
|
task_id: task.id,
|
|
151
259
|
};
|
|
152
260
|
}
|
|
153
|
-
if (
|
|
261
|
+
if (change) return { change_id: change.id };
|
|
154
262
|
const baseline = baselines.readBaseline(db);
|
|
155
263
|
return baseline ? { baseline_id: baseline.id } : { project_id: 'project' };
|
|
156
264
|
}
|
|
@@ -159,7 +267,28 @@ function readContext(db, input = {}, {
|
|
|
159
267
|
rootDir = process.cwd(),
|
|
160
268
|
runtime = 'unknown',
|
|
161
269
|
} = {}) {
|
|
162
|
-
|
|
270
|
+
const result = contextEnvelope.buildEnvelope(db, input, { rootDir, runtime });
|
|
271
|
+
const rejectedAttempts = rejectedAttemptAudit(db);
|
|
272
|
+
let visible = {
|
|
273
|
+
...result,
|
|
274
|
+
audit: {
|
|
275
|
+
rejected_attempts: rejectedAttempts,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
delete visible.bytes;
|
|
279
|
+
const limit = input.detail === 'full'
|
|
280
|
+
? contextEnvelope.FULL_LIMIT
|
|
281
|
+
: contextEnvelope.SUMMARY_LIMIT;
|
|
282
|
+
while (Buffer.byteLength(JSON.stringify(visible)) > limit
|
|
283
|
+
&& rejectedAttempts.length > 0) {
|
|
284
|
+
rejectedAttempts.pop();
|
|
285
|
+
}
|
|
286
|
+
if (Buffer.byteLength(JSON.stringify(visible)) > limit) {
|
|
287
|
+
visible = { ...result };
|
|
288
|
+
delete visible.bytes;
|
|
289
|
+
}
|
|
290
|
+
visible.bytes = Buffer.byteLength(JSON.stringify(visible));
|
|
291
|
+
return visible;
|
|
163
292
|
}
|
|
164
293
|
|
|
165
294
|
function initializeProject(data = {}) {
|
|
@@ -184,12 +313,16 @@ function baselineRecord(db, action, data, rootDir) {
|
|
|
184
313
|
const result = baselines.convergeBaseline(db, data, { rootDir });
|
|
185
314
|
return {
|
|
186
315
|
...result,
|
|
187
|
-
diagnostics:
|
|
188
|
-
|
|
189
|
-
: (result.blockers || []).map((code) => ({
|
|
316
|
+
diagnostics: [
|
|
317
|
+
...(result.blockers || []).map((code) => ({
|
|
190
318
|
code,
|
|
191
319
|
severity: 'needs_attention',
|
|
192
320
|
})),
|
|
321
|
+
...(result.warnings || []).map((code) => ({
|
|
322
|
+
code,
|
|
323
|
+
severity: 'warning',
|
|
324
|
+
})),
|
|
325
|
+
],
|
|
193
326
|
};
|
|
194
327
|
}
|
|
195
328
|
throw Object.assign(new Error(`unsupported baseline action: ${action}`), {
|
|
@@ -199,7 +332,7 @@ function baselineRecord(db, action, data, rootDir) {
|
|
|
199
332
|
|
|
200
333
|
function changeContractRecord(db, action, data, rootDir) {
|
|
201
334
|
if (action === 'open') {
|
|
202
|
-
return changes.createChange(db, data, { rootDir
|
|
335
|
+
return changes.createChange(db, data, { rootDir });
|
|
203
336
|
}
|
|
204
337
|
if (action === 'revise') {
|
|
205
338
|
return {
|
|
@@ -207,7 +340,7 @@ function changeContractRecord(db, action, data, rootDir) {
|
|
|
207
340
|
db,
|
|
208
341
|
data.id,
|
|
209
342
|
data.patch || {},
|
|
210
|
-
{ rootDir
|
|
343
|
+
{ rootDir },
|
|
211
344
|
),
|
|
212
345
|
};
|
|
213
346
|
}
|
|
@@ -217,7 +350,7 @@ function changeContractRecord(db, action, data, rootDir) {
|
|
|
217
350
|
db,
|
|
218
351
|
data.id,
|
|
219
352
|
{ status: 'cancelled' },
|
|
220
|
-
{ rootDir
|
|
353
|
+
{ rootDir },
|
|
221
354
|
),
|
|
222
355
|
};
|
|
223
356
|
}
|
|
@@ -332,6 +465,17 @@ function applyRecord(db, entry, { rootDir }) {
|
|
|
332
465
|
if (kind === 'task_contract') return taskContractRecord(db, action, data);
|
|
333
466
|
if (kind === 'task_outcome') return taskOutcomeRecord(db, action, data, rootDir);
|
|
334
467
|
if (kind === 'artifact' && action === 'bind') {
|
|
468
|
+
const legacyWorkflowReference = data.owner_type === 'workflow'
|
|
469
|
+
|| [...(data.source_refs || []), ...(data.consumer_refs || [])]
|
|
470
|
+
.some((reference) => reference?.type === 'workflow');
|
|
471
|
+
if (legacyWorkflowReference) {
|
|
472
|
+
throw Object.assign(
|
|
473
|
+
new Error(
|
|
474
|
+
'workflow-owned artifact authority is retired and remains read-only history',
|
|
475
|
+
),
|
|
476
|
+
{ code: 'LEGACY_AUTHORITY_READ_ONLY' },
|
|
477
|
+
);
|
|
478
|
+
}
|
|
335
479
|
return artifactRegistry.recordArtifact(db, data, { rootDir });
|
|
336
480
|
}
|
|
337
481
|
if (kind === 'event' && action === 'append') {
|
|
@@ -423,14 +567,17 @@ function checkpointDiagnostics(input, context) {
|
|
|
423
567
|
}));
|
|
424
568
|
}
|
|
425
569
|
|
|
426
|
-
function
|
|
570
|
+
function stageAdvisoryDiagnostics(db, input, scope) {
|
|
427
571
|
const payload = input.payload || {};
|
|
428
572
|
const evidence = Array.isArray(payload.evidence) ? payload.evidence : [];
|
|
429
|
-
const
|
|
573
|
+
const warning = (code, message) => ({ code, severity: 'warning', message });
|
|
430
574
|
if (input.stage === 'research') {
|
|
431
575
|
return evidence.length > 0
|
|
432
576
|
? []
|
|
433
|
-
: [
|
|
577
|
+
: [warning(
|
|
578
|
+
'RESEARCH_EVIDENCE_MISSING',
|
|
579
|
+
'No research evidence was declared; the caller remains responsible for semantic sufficiency.',
|
|
580
|
+
)];
|
|
434
581
|
}
|
|
435
582
|
if (input.stage === 'dev') {
|
|
436
583
|
const task = scope.task_id ? ops.readTask(db, scope.task_id) : null;
|
|
@@ -442,14 +589,14 @@ function stageReadinessDiagnostics(db, input, scope) {
|
|
|
442
589
|
).get(task.id)
|
|
443
590
|
: null;
|
|
444
591
|
const diagnostics = [];
|
|
445
|
-
if (!task) diagnostics.push(
|
|
592
|
+
if (!task) diagnostics.push(warning('TASK_NOT_FOUND', 'No task scope was found for this Dev checkpoint.'));
|
|
446
593
|
else if (task.status !== 'completed') {
|
|
447
|
-
diagnostics.push(
|
|
594
|
+
diagnostics.push(warning('TASK_OUTCOME_INCOMPLETE', `Task ${task.id} is not completed.`));
|
|
448
595
|
}
|
|
449
596
|
if (task && !outcome) {
|
|
450
|
-
diagnostics.push(
|
|
451
|
-
'
|
|
452
|
-
`Task ${task.id}
|
|
597
|
+
diagnostics.push(warning(
|
|
598
|
+
'WORKER_OUTCOME_MISSING',
|
|
599
|
+
`Task ${task.id} has no current Worker Packet outcome artifact.`,
|
|
453
600
|
));
|
|
454
601
|
}
|
|
455
602
|
return diagnostics;
|
|
@@ -457,22 +604,32 @@ function stageReadinessDiagnostics(db, input, scope) {
|
|
|
457
604
|
if (input.stage === 'test') {
|
|
458
605
|
const result = String(payload.result || '').toLowerCase();
|
|
459
606
|
const diagnostics = [];
|
|
460
|
-
if (!
|
|
461
|
-
diagnostics.push(
|
|
607
|
+
if (!result) {
|
|
608
|
+
diagnostics.push(warning('TEST_RESULT_MISSING', 'No test result was declared.'));
|
|
609
|
+
} else if (!['pass', 'known_red'].includes(result)) {
|
|
610
|
+
diagnostics.push(warning(
|
|
611
|
+
'TEST_RESULT_RECORDED',
|
|
612
|
+
`The caller recorded test result ${result}; MCP does not reinterpret it.`,
|
|
613
|
+
));
|
|
462
614
|
}
|
|
463
615
|
if (evidence.length === 0) {
|
|
464
|
-
diagnostics.push(
|
|
616
|
+
diagnostics.push(warning('TEST_EVIDENCE_MISSING', 'No test evidence was declared.'));
|
|
465
617
|
}
|
|
466
618
|
return diagnostics;
|
|
467
619
|
}
|
|
468
620
|
if (input.stage === 'review') {
|
|
469
621
|
const verdict = String(payload.verdict || '').toLowerCase();
|
|
470
622
|
const diagnostics = [];
|
|
471
|
-
if (!
|
|
472
|
-
diagnostics.push(
|
|
623
|
+
if (!verdict) {
|
|
624
|
+
diagnostics.push(warning('REVIEW_VERDICT_MISSING', 'No review verdict was declared.'));
|
|
625
|
+
} else if (!['approve', 'pass'].includes(verdict)) {
|
|
626
|
+
diagnostics.push(warning(
|
|
627
|
+
'REVIEW_VERDICT_RECORDED',
|
|
628
|
+
`The caller recorded review verdict ${verdict}; MCP does not reinterpret it.`,
|
|
629
|
+
));
|
|
473
630
|
}
|
|
474
631
|
if (evidence.length === 0) {
|
|
475
|
-
diagnostics.push(
|
|
632
|
+
diagnostics.push(warning('REVIEW_EVIDENCE_MISSING', 'No review evidence was declared.'));
|
|
476
633
|
}
|
|
477
634
|
return diagnostics;
|
|
478
635
|
}
|
|
@@ -485,23 +642,87 @@ function stageReadinessDiagnostics(db, input, scope) {
|
|
|
485
642
|
{ change_id: scope.change_id },
|
|
486
643
|
{ includeDraft: false },
|
|
487
644
|
)) {
|
|
488
|
-
diagnostics.push(
|
|
489
|
-
`${stage.toUpperCase()}
|
|
490
|
-
`
|
|
645
|
+
diagnostics.push(warning(
|
|
646
|
+
`${stage.toUpperCase()}_CHECKPOINT_MISSING`,
|
|
647
|
+
`No accepted ${stage} checkpoint was found; the caller owns the delivery decision.`,
|
|
491
648
|
));
|
|
492
649
|
}
|
|
493
650
|
}
|
|
494
651
|
if (String(payload.summary || '').trim().length < 3) {
|
|
495
|
-
diagnostics.push(
|
|
652
|
+
diagnostics.push(warning('DELIVERY_SUMMARY_MISSING', 'No durable delivery summary was declared.'));
|
|
496
653
|
}
|
|
497
654
|
if (evidence.length === 0) {
|
|
498
|
-
diagnostics.push(
|
|
655
|
+
diagnostics.push(warning('DELIVERY_EVIDENCE_MISSING', 'No delivery evidence was declared.'));
|
|
499
656
|
}
|
|
500
657
|
return diagnostics;
|
|
501
658
|
}
|
|
502
659
|
return [];
|
|
503
660
|
}
|
|
504
661
|
|
|
662
|
+
function normalizeCheckpointEvidence(db, input, rootDir) {
|
|
663
|
+
const evidence = Array.isArray(input.payload?.evidence) ? input.payload.evidence : [];
|
|
664
|
+
return evidence.map((item, index) => {
|
|
665
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
666
|
+
const error = new Error(`checkpoint evidence[${index}] must be an object`);
|
|
667
|
+
error.code = 'VALIDATION_ERROR';
|
|
668
|
+
throw error;
|
|
669
|
+
}
|
|
670
|
+
const normalized = { ...item };
|
|
671
|
+
let relative = typeof item.ref === 'string' ? item.ref.trim() : '';
|
|
672
|
+
let artifact = null;
|
|
673
|
+
if (item.artifact_id) {
|
|
674
|
+
try {
|
|
675
|
+
artifact = artifactRegistry.getArtifact(db, { id: String(item.artifact_id) });
|
|
676
|
+
} catch (error) {
|
|
677
|
+
if (error?.code !== 'ARTIFACT_NOT_FOUND') throw error;
|
|
678
|
+
}
|
|
679
|
+
if (!artifact || artifact.status !== 'current') {
|
|
680
|
+
const error = new Error(`checkpoint evidence artifact is not current: ${item.artifact_id}`);
|
|
681
|
+
error.code = 'CHECKPOINT_EVIDENCE_AUTHORITY_MISSING';
|
|
682
|
+
throw error;
|
|
683
|
+
}
|
|
684
|
+
if (relative && artifact.path !== relative) {
|
|
685
|
+
const error = new Error(`checkpoint evidence path does not match artifact ${item.artifact_id}`);
|
|
686
|
+
error.code = 'CHECKPOINT_EVIDENCE_AUTHORITY_MISSING';
|
|
687
|
+
throw error;
|
|
688
|
+
}
|
|
689
|
+
relative = artifact.path;
|
|
690
|
+
normalized.artifact_id = artifact.id;
|
|
691
|
+
if (item.digest && String(item.digest) !== artifact.digest) {
|
|
692
|
+
const error = new Error(
|
|
693
|
+
`checkpoint evidence digest contradicts artifact ${item.artifact_id}`,
|
|
694
|
+
);
|
|
695
|
+
error.code = 'CHECKPOINT_EVIDENCE_DIGEST_MISMATCH';
|
|
696
|
+
error.details = {
|
|
697
|
+
path: relative,
|
|
698
|
+
expected: artifact.digest,
|
|
699
|
+
actual: String(item.digest),
|
|
700
|
+
};
|
|
701
|
+
throw error;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (!item.digest && !item.artifact_id) return normalized;
|
|
705
|
+
if (!relative) {
|
|
706
|
+
const error = new Error(`checkpoint evidence[${index}] requires ref when digest is supplied`);
|
|
707
|
+
error.code = 'VALIDATION_ERROR';
|
|
708
|
+
throw error;
|
|
709
|
+
}
|
|
710
|
+
const read = readStableProjectFile(rootDir, relative);
|
|
711
|
+
const expected = item.artifact_id
|
|
712
|
+
? artifact.digest
|
|
713
|
+
: String(item.digest);
|
|
714
|
+
if (read.digest !== expected) {
|
|
715
|
+
const error = new Error(`checkpoint evidence digest mismatch: ${relative}`);
|
|
716
|
+
error.code = 'CHECKPOINT_EVIDENCE_DIGEST_MISMATCH';
|
|
717
|
+
error.details = { path: relative, expected, actual: read.digest };
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
normalized.ref = relative;
|
|
721
|
+
normalized.digest = read.digest;
|
|
722
|
+
return normalized;
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
|
|
505
726
|
async function checkpoint(db, input = {}, {
|
|
506
727
|
rootDir = process.cwd(),
|
|
507
728
|
runtime = 'unknown',
|
|
@@ -517,14 +738,22 @@ async function checkpoint(db, input = {}, {
|
|
|
517
738
|
scope,
|
|
518
739
|
detail: 'summary',
|
|
519
740
|
}, { rootDir, runtime });
|
|
741
|
+
const evidence = normalizeCheckpointEvidence(db, input, rootDir);
|
|
520
742
|
const diagnostics = [
|
|
521
743
|
...checkpointDiagnostics(input, view),
|
|
522
|
-
...
|
|
744
|
+
...stageAdvisoryDiagnostics(db, input, scope),
|
|
745
|
+
...evidence
|
|
746
|
+
.filter((item) => !item.digest && !item.artifact_id)
|
|
747
|
+
.map((item, index) => ({
|
|
748
|
+
code: 'CHECKPOINT_EVIDENCE_UNBOUND',
|
|
749
|
+
severity: 'warning',
|
|
750
|
+
message: `Evidence declaration ${item.ref || item.kind || index} has no current digest or managed artifact binding.`,
|
|
751
|
+
})),
|
|
523
752
|
];
|
|
524
753
|
let result = null;
|
|
525
754
|
|
|
526
755
|
if (input.stage === 'plan'
|
|
527
|
-
&& !diagnostics.some((item) => item.severity
|
|
756
|
+
&& !diagnostics.some((item) => item.severity === 'hard_conflict')) {
|
|
528
757
|
try {
|
|
529
758
|
result = planCheckpoint.publishPlan(db, {
|
|
530
759
|
change_id: scope.change_id,
|
|
@@ -532,7 +761,7 @@ async function checkpoint(db, input = {}, {
|
|
|
532
761
|
}, { rootDir, markRecoveryRequired: markPlanRecoveryRequired });
|
|
533
762
|
} catch (error) {
|
|
534
763
|
if (isHardError(error)) throw error;
|
|
535
|
-
diagnostics.push(diagnostic(error));
|
|
764
|
+
diagnostics.push(diagnostic(error, 'hard_conflict'));
|
|
536
765
|
}
|
|
537
766
|
}
|
|
538
767
|
|
|
@@ -548,12 +777,12 @@ async function checkpoint(db, input = {}, {
|
|
|
548
777
|
payload: input.stage === 'plan' && result
|
|
549
778
|
? { ...(input.payload || {}), plan: result }
|
|
550
779
|
: (input.payload || {}),
|
|
551
|
-
evidence
|
|
780
|
+
evidence,
|
|
552
781
|
diagnostics,
|
|
553
782
|
context_envelope_id: context.id,
|
|
554
783
|
idempotency_key: `${input.idempotency_key}:draft`,
|
|
555
784
|
});
|
|
556
|
-
if (diagnostics.some((item) => item.severity
|
|
785
|
+
if (diagnostics.some((item) => item.severity === 'hard_conflict')) {
|
|
557
786
|
return {
|
|
558
787
|
accepted: false,
|
|
559
788
|
mutable: true,
|
|
@@ -770,7 +999,7 @@ async function archive(db, input = {}, {
|
|
|
770
999
|
const result = changes.archiveChange(
|
|
771
1000
|
db,
|
|
772
1001
|
{ id: input.change_id, ...(input.payload || {}) },
|
|
773
|
-
{ rootDir
|
|
1002
|
+
{ rootDir },
|
|
774
1003
|
);
|
|
775
1004
|
const output = {
|
|
776
1005
|
accepted: true,
|
|
@@ -802,22 +1031,26 @@ async function archive(db, input = {}, {
|
|
|
802
1031
|
}
|
|
803
1032
|
|
|
804
1033
|
async function dispatch(name, input, db, context = {}) {
|
|
805
|
-
|
|
806
|
-
if (name === 'ultra.
|
|
807
|
-
if (name === 'ultra.
|
|
808
|
-
if (name === 'ultra.
|
|
809
|
-
if (name === 'ultra.
|
|
810
|
-
if (name === 'ultra.
|
|
1034
|
+
let result;
|
|
1035
|
+
if (name === 'ultra.context') result = readContext(db, input, context);
|
|
1036
|
+
else if (name === 'ultra.record') result = await record(db, input, context);
|
|
1037
|
+
else if (name === 'ultra.checkpoint') result = await checkpoint(db, input, context);
|
|
1038
|
+
else if (name === 'ultra.sync') result = sync(db, input, context);
|
|
1039
|
+
else if (name === 'ultra.session') result = await session(db, input, context);
|
|
1040
|
+
else if (name === 'ultra.archive') result = await archive(db, input, context);
|
|
811
1041
|
if (name === 'ultra.doctor') {
|
|
812
|
-
|
|
1042
|
+
result = doctor.runDoctor(db, {
|
|
813
1043
|
rootDir: context.rootDir || process.cwd(),
|
|
814
1044
|
repair: input.repair === true,
|
|
815
1045
|
project: context.projector,
|
|
816
1046
|
});
|
|
817
1047
|
}
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1048
|
+
if (result === undefined) {
|
|
1049
|
+
const error = new Error(`unhandled public tool ${name}`);
|
|
1050
|
+
error.code = 'UNKNOWN_TOOL';
|
|
1051
|
+
throw error;
|
|
1052
|
+
}
|
|
1053
|
+
return rememberRejectedAttempt(db, name, input, result);
|
|
821
1054
|
}
|
|
822
1055
|
|
|
823
1056
|
module.exports = {
|
package/mcp-server/server.cjs
CHANGED
|
@@ -284,7 +284,7 @@ function startServer({
|
|
|
284
284
|
);
|
|
285
285
|
const own = processed.jobs.find((item) => item.id === job.id);
|
|
286
286
|
runtimeMeta = {
|
|
287
|
-
|
|
287
|
+
projection_commit: 'committed',
|
|
288
288
|
projection_status: own?.status || 'failed',
|
|
289
289
|
projection_job_id: job.id,
|
|
290
290
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ultra-builder-pro-cli",
|
|
3
|
-
"version": "0.24.
|
|
3
|
+
"version": "0.24.2",
|
|
4
4
|
"description": "Explicitly invoked Ultra Builder Pro workflows for Claude Code, OpenCode, Codex CLI, Kimi Code, and Grok Build, with a project-local MCP persistence and safety kernel.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ultra-builder-pro-cli": "bin/install.js",
|
|
@@ -39,7 +39,8 @@ manufacturing a mutation.
|
|
|
39
39
|
|
|
40
40
|
Changing accepted intent creates a new accepted checkpoint revision and makes only
|
|
41
41
|
genuinely dependent evidence stale. Readiness is derived, not a stored absorbing
|
|
42
|
-
state.
|
|
42
|
+
state. Semantic diagnostics remain advisory; structural, digest, path, or concurrency
|
|
43
|
+
conflicts require repair and retry of the same draft.
|
|
43
44
|
Use `change_contract / cancel` only when the owner intentionally abandons the Change.
|
|
44
45
|
|
|
45
46
|
Publish the durable Change contract with `ultra.sync { action: publish }` when it is a
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: ultra-deliver
|
|
3
|
-
description:
|
|
3
|
+
description: Reconcile and archive an explicitly accepted Ultra Change with recoverable local evidence. Use when the caller is ready to make a local delivery handoff with current managed evidence and an explicit rationale for any omitted capability.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Converge and archive local authority
|
|
@@ -12,8 +12,10 @@ publication, deployment, or another external effect.
|
|
|
12
12
|
|
|
13
13
|
1. Call `ultra.context { stage: deliver, scope: { change_id }, detail: full }`.
|
|
14
14
|
2. Import a newer team checkpoint with `ultra.sync`; stop on a real conflict.
|
|
15
|
-
3.
|
|
16
|
-
|
|
15
|
+
3. Inspect current task outcomes, tests, both review axes, accepted decisions, typed
|
|
16
|
+
delta, documentation reconciliation, and checkout evidence. Decide whether the
|
|
17
|
+
actual Change is sufficiently complete; explicitly record any omitted stage and its
|
|
18
|
+
evidence-based rationale instead of inventing a fixed recipe.
|
|
17
19
|
4. Resolve every specification-learning candidate. Keep updates inside the Change
|
|
18
20
|
overlay until archive.
|
|
19
21
|
5. Write and register documentation reconciliation and an
|
|
@@ -27,20 +29,16 @@ Use one typed `ultra.record` batch for `artifact / bind`, any normalized
|
|
|
27
29
|
## Archive once
|
|
28
30
|
|
|
29
31
|
Call `ultra.archive` once with the Change id, stable idempotency key, archive summary,
|
|
30
|
-
reconciliation fields, delivery evidence steps, and report output.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
-
|
|
40
|
-
|
|
41
|
-
A semantic rejection returns mutable blockers. Repair the same packet and retry. A
|
|
42
|
-
hard corruption, path, concurrency, or recovery error remains fail-closed and routes to
|
|
43
|
-
`ultra-doctor`.
|
|
32
|
+
reconciliation fields, delivery evidence steps, explicit omissions, and report output.
|
|
33
|
+
The model's report is the semantic handoff. MCP validates its current registered
|
|
34
|
+
authority, paths, digests, reconciliation structure, and idempotency; applies the
|
|
35
|
+
overlay and archive through the recoverable filesystem/DB transaction; rebinds
|
|
36
|
+
registered artifacts into a self-contained archive; and publishes the updated team
|
|
37
|
+
checkpoint. It does not require a hard-coded Plan/Dev/Test/Review/Deliver sequence.
|
|
38
|
+
|
|
39
|
+
Semantic warnings remain in the archive history but do not reject an explicit local
|
|
40
|
+
handoff. Corruption, unsafe paths, digest/CAS conflicts, permissions, or recovery
|
|
41
|
+
failure remain fail-closed and route to `ultra-doctor`.
|
|
44
42
|
|
|
45
43
|
Verify the archived packet, baseline, checkpoint, and rollback state. Handle any
|
|
46
44
|
separately authorized Git or release effect only after local delivery and report it as
|
|
@@ -45,7 +45,9 @@ Fix blocking findings and refresh affected evidence.
|
|
|
45
45
|
|
|
46
46
|
Call `ultra.checkpoint` once with `stage: dev`, the exact Change/Task scope, evidence,
|
|
47
47
|
and stable idempotency key. It binds the exact Context Envelope and Worker Packet and
|
|
48
|
-
reports outcome diagnostics.
|
|
48
|
+
reports outcome diagnostics. The model decides whether the implemented slice satisfies
|
|
49
|
+
the Task; semantic warnings remain visible. A rejection means the declared authority,
|
|
50
|
+
path, digest, or concurrency boundary was unsafe and leaves the draft mutable.
|
|
49
51
|
|
|
50
52
|
After acceptance, mark the task durably completed, publish with `ultra.sync`, and
|
|
51
53
|
create at most one authorized local task commit containing code and semantic
|