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.
@@ -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: statusEventType(nextStatus),
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: { from: current.status, to: nextStatus },
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(db, { change_id: parent.change_id }, { rootDir });
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
- if (scope.task_id) {
146
- const task = ops.readTask(db, scope.task_id);
147
- if (!task) return { task_id: scope.task_id };
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: scope.change_id || task.change_id || undefined,
257
+ ...(changeId ? { change_id: changeId } : {}),
150
258
  task_id: task.id,
151
259
  };
152
260
  }
153
- if (scope.change_id) return { change_id: scope.change_id };
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
- return contextEnvelope.buildEnvelope(db, input, { rootDir, runtime });
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: result.ready
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, kernelMode: true });
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, kernelMode: true },
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, kernelMode: true },
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 stageReadinessDiagnostics(db, input, scope) {
570
+ function stageAdvisoryDiagnostics(db, input, scope) {
427
571
  const payload = input.payload || {};
428
572
  const evidence = Array.isArray(payload.evidence) ? payload.evidence : [];
429
- const needs = (code, message) => ({ code, severity: 'needs_attention', message });
573
+ const warning = (code, message) => ({ code, severity: 'warning', message });
430
574
  if (input.stage === 'research') {
431
575
  return evidence.length > 0
432
576
  ? []
433
- : [needs('RESEARCH_EVIDENCE_REQUIRED', 'Research checkpoint requires at least one evidence reference.')];
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(needs('TASK_NOT_FOUND', 'Dev checkpoint requires a task scope.'));
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(needs('TASK_OUTCOME_INCOMPLETE', `Task ${task.id} is not completed.`));
594
+ diagnostics.push(warning('TASK_OUTCOME_INCOMPLETE', `Task ${task.id} is not completed.`));
448
595
  }
449
596
  if (task && !outcome) {
450
- diagnostics.push(needs(
451
- 'WORKER_OUTCOME_REQUIRED',
452
- `Task ${task.id} requires an artifact bound to its exact Worker Packet.`,
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 (!['pass', 'known_red'].includes(result)) {
461
- diagnostics.push(needs('TEST_RESULT_REQUIRED', 'Test checkpoint requires result=pass or known_red.'));
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(needs('TEST_EVIDENCE_REQUIRED', 'Test checkpoint requires evidence.'));
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 (!['approve', 'pass'].includes(verdict)) {
472
- diagnostics.push(needs('REVIEW_VERDICT_REQUIRED', 'Review checkpoint requires an approved verdict.'));
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(needs('REVIEW_EVIDENCE_REQUIRED', 'Review checkpoint requires evidence.'));
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(needs(
489
- `${stage.toUpperCase()}_CHECKPOINT_REQUIRED`,
490
- `Deliver checkpoint requires an accepted ${stage} checkpoint.`,
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(needs('DELIVERY_SUMMARY_REQUIRED', 'Deliver checkpoint requires a summary.'));
652
+ diagnostics.push(warning('DELIVERY_SUMMARY_MISSING', 'No durable delivery summary was declared.'));
496
653
  }
497
654
  if (evidence.length === 0) {
498
- diagnostics.push(needs('DELIVERY_EVIDENCE_REQUIRED', 'Deliver checkpoint requires evidence.'));
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
- ...stageReadinessDiagnostics(db, input, scope),
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 !== 'warning')) {
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: Array.isArray(input.payload?.evidence) ? input.payload.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 !== 'warning')) {
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, kernelMode: true },
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
- if (name === 'ultra.context') return readContext(db, input, context);
806
- if (name === 'ultra.record') return record(db, input, context);
807
- if (name === 'ultra.checkpoint') return checkpoint(db, input, context);
808
- if (name === 'ultra.sync') return sync(db, input, context);
809
- if (name === 'ultra.session') return session(db, input, context);
810
- if (name === 'ultra.archive') return archive(db, input, context);
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
- return doctor.runDoctor(db, {
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
- const error = new Error(`unhandled public tool ${name}`);
819
- error.code = 'UNKNOWN_TOOL';
820
- throw error;
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 = {
@@ -284,7 +284,7 @@ function startServer({
284
284
  );
285
285
  const own = processed.jobs.find((item) => item.id === job.id);
286
286
  runtimeMeta = {
287
- state_commit: 'committed',
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.0",
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. A semantic rejection returns diagnostics; repair and retry the same draft.
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: Converge a verified Ultra Change, reconcile baseline specifications, and archive it with recoverable local evidence. Use when implementation, testing, and review are current.
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. Require current task outcomes, test report, both review axes, accepted decisions,
16
- typed delta, documentation reconciliation, and current checkout evidence.
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. The operation
31
- internally:
32
-
33
- - compiles or reuses convergence context;
34
- - checks the deliver draft;
35
- - derives convergence from current dev, test, review, delta, docs, and checkout;
36
- - applies the overlay and archive through the recoverable filesystem/DB transaction;
37
- - rebinds registered artifacts into a self-contained archive;
38
- - accepts one immutable delivery checkpoint revision;
39
- - publishes the updated team checkpoint.
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. A rejection leaves the draft mutable.
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