ultra-builder-pro-cli 0.24.1 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/README.md +10 -0
  3. package/adapters/claude.js +104 -0
  4. package/adapters/codex.js +363 -40
  5. package/adapters/grok.js +1254 -58
  6. package/adapters/kimi.js +682 -13
  7. package/adapters/opencode.js +132 -11
  8. package/agents/code-reviewer.md +4 -5
  9. package/agents/debugger.md +8 -8
  10. package/docs/AGENT-CONTEXT.md +30 -4
  11. package/docs/ARCHITECTURE.md +24 -5
  12. package/docs/ARTIFACT-AUTHORITY.md +13 -0
  13. package/docs/COMMIT-HASH-BACKFILL.md +25 -18
  14. package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
  15. package/docs/STATE-DB-ACCESS-POLICY.md +8 -1
  16. package/mcp-server/lib/artifact-registry.cjs +188 -107
  17. package/mcp-server/lib/baseline-workflow.cjs +160 -23
  18. package/mcp-server/lib/change-packet.cjs +4 -32
  19. package/mcp-server/lib/change-workflow.cjs +36 -2755
  20. package/mcp-server/lib/context-envelope.cjs +124 -51
  21. package/mcp-server/lib/decision-records.cjs +164 -78
  22. package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
  23. package/mcp-server/lib/legacy-change-workflow.cjs +3032 -0
  24. package/mcp-server/lib/managed-file-write.cjs +56 -0
  25. package/mcp-server/lib/prd-parser.cjs +2 -2
  26. package/mcp-server/lib/stage-checkpoints.cjs +163 -9
  27. package/mcp-server/lib/state-db.cjs +186 -8
  28. package/mcp-server/lib/state-ops.cjs +214 -31
  29. package/mcp-server/lib/task-contract.cjs +404 -0
  30. package/mcp-server/lib/task-expander.cjs +8 -3
  31. package/mcp-server/lib/task-ledger.cjs +175 -62
  32. package/mcp-server/lib/ultra-facade.cjs +1895 -172
  33. package/mcp-server/lib/worker-packet.cjs +2 -2
  34. package/mcp-server/server.cjs +24 -5
  35. package/orchestrator/parallel-orchestrator.cjs +114 -27
  36. package/orchestrator/session-runner.cjs +25 -136
  37. package/package.json +1 -1
  38. package/skills/ultra-change/SKILL.md +6 -1
  39. package/skills/ultra-dev/SKILL.md +9 -4
  40. package/skills/ultra-plan/SKILL.md +13 -4
  41. package/skills/ultra-plan/references/semantic-preflight.md +6 -4
  42. package/spec/mcp-tools.yaml +114 -9
  43. package/spec/schemas/context-file.v4.5.schema.json +2 -2
  44. package/spec/schemas/state-db.sql +10 -6
  45. package/spec/schemas/task-ledger.v2.schema.json +23 -8
  46. package/spec/schemas/tasks.v4.5.schema.json +34 -12
@@ -13,8 +13,14 @@ const fs = require('node:fs');
13
13
  const path = require('node:path');
14
14
  const { spawnSync } = require('node:child_process');
15
15
 
16
+ const canonical = require('./canonical-json.cjs');
16
17
  const { openStateDb } = require('./state-db.cjs');
18
+ const {
19
+ TASK_CONTRACT_DEFINE_FIELDS,
20
+ TASK_CONTRACT_PATCH_FIELDS,
21
+ } = require('./task-contract.cjs');
17
22
  const { isSupportedRuntime } = require('../../adapters/_shared/runtime-assets.cjs');
23
+ const closeJournal = require('../../orchestrator/session-close-journal.cjs');
18
24
 
19
25
  const STATUS_TRANSITIONS = Object.freeze({
20
26
  pending: new Set(['in_progress', 'blocked', 'expanded']),
@@ -35,10 +41,8 @@ const TASK_FIELDS = Object.freeze([
35
41
  ]);
36
42
 
37
43
  const PATCHABLE_FIELDS = Object.freeze([
38
- 'priority', 'complexity', 'estimated_days', 'deps', 'files_modified',
39
- 'session_id', 'stale', 'tag', 'trace_to',
40
- 'outcome', 'slice_kind', 'public_seam', 'verification_command',
41
- 'acceptance', 'context_refs', 'docs_impact', 'ownership',
44
+ ...TASK_CONTRACT_PATCH_FIELDS,
45
+ 'session_id', 'stale', 'tag',
42
46
  'context_file', 'completion_commit',
43
47
  'change_id',
44
48
  ]);
@@ -48,11 +52,7 @@ const STALE_RECONCILIATION_FIELDS = Object.freeze([
48
52
  'verification_command', 'acceptance', 'context_refs', 'docs_impact', 'ownership',
49
53
  ]);
50
54
 
51
- const TASK_CONTRACT_PATCH_FIELDS = new Set([
52
- 'priority', 'complexity', 'estimated_days', 'deps', 'files_modified', 'tag',
53
- 'trace_to', 'outcome', 'slice_kind', 'public_seam', 'verification_command',
54
- 'acceptance', 'context_refs', 'docs_impact', 'ownership',
55
- ]);
55
+ const TASK_CONTRACT_PATCH_FIELD_SET = new Set(TASK_CONTRACT_PATCH_FIELDS);
56
56
 
57
57
  const SESSION_PATCHABLE = Object.freeze([
58
58
  'pid', 'status', 'lease_expires_at', 'heartbeat_at', 'worktree_path', 'artifact_dir',
@@ -142,7 +142,7 @@ function rowToTask(row) {
142
142
  delete out[column];
143
143
  }
144
144
  if (out.stale !== undefined && out.stale !== null) {
145
- out.stale = Boolean(out.stale);
145
+ if (out.stale === 0 || out.stale === 1) out.stale = Boolean(out.stale);
146
146
  }
147
147
  return out;
148
148
  }
@@ -197,11 +197,17 @@ function normalizeContextRefs(value) {
197
197
  `context_refs[${index}].freshness_policy must be digest, existence, or advisory`,
198
198
  );
199
199
  }
200
+ if (item.required !== undefined && typeof item.required !== 'boolean') {
201
+ throw new StateOpsError(
202
+ 'VALIDATION_ERROR',
203
+ `context_refs[${index}].required must be a boolean`,
204
+ );
205
+ }
200
206
  const normalized = {
201
207
  ref,
202
208
  reason,
203
209
  kind,
204
- required: item.required !== false,
210
+ required: item.required === undefined ? true : item.required,
205
211
  freshness_policy: freshnessPolicy,
206
212
  };
207
213
  if (expectedDigest !== null) normalized.expected_digest = String(expectedDigest);
@@ -254,7 +260,7 @@ function normalizeOwnership(value) {
254
260
  function taskContractBlockers(task) {
255
261
  const blockers = [];
256
262
  if (!String(task?.outcome || '').trim()) blockers.push('TASK_OUTCOME_MISSING');
257
- if (!['tracer_bullet', 'expand_contract', 'integration_checkpoint'].includes(task?.slice_kind)) {
263
+ if (!String(task?.slice_kind || '').trim()) {
258
264
  blockers.push('TASK_SLICE_KIND_MISSING');
259
265
  }
260
266
  if (!String(task?.public_seam || '').trim()) blockers.push('TASK_PUBLIC_SEAM_MISSING');
@@ -548,8 +554,6 @@ function patchTask(db, id, patch = {}) {
548
554
  params.push(id);
549
555
  db.prepare(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ?`).run(...params);
550
556
  const updated = readTask(db, id);
551
- if (clearingStale) assertTaskExecutionContract(updated);
552
-
553
557
  if (nextStatus && nextStatus !== current.status) {
554
558
  appendEventInTx(db, {
555
559
  type: current.status === 'completed' && nextStatus === 'in_progress'
@@ -566,7 +570,9 @@ function patchTask(db, id, patch = {}) {
566
570
  },
567
571
  });
568
572
  }
569
- const contractFields = Object.keys(patch).filter((field) => TASK_CONTRACT_PATCH_FIELDS.has(field));
573
+ const contractFields = Object.keys(patch).filter(
574
+ (field) => TASK_CONTRACT_PATCH_FIELD_SET.has(field),
575
+ );
570
576
  if (contractFields.length > 0) {
571
577
  appendEventInTx(db, {
572
578
  type: clearingStale ? 'task_contract_reconciled' : 'task_contract_updated',
@@ -597,37 +603,214 @@ function updateTaskStatus(db, id, nextStatus) {
597
603
  return patchTask(db, id, { status: nextStatus });
598
604
  }
599
605
 
600
- function deleteTask(db, id, { force = false } = {}) {
606
+ function deleteTask(db, id, { force = false, rootDir = null } = {}) {
601
607
  return tx(db, () => {
602
- const t = readTask(db, id);
603
- if (!t) throw new StateOpsError('TASK_NOT_FOUND', `no task ${id}`);
604
- if (t.session_id && !force) {
605
- throw new StateOpsError('SESSION_ACTIVE', `task ${id} has session ${t.session_id}; pass force=true to override`);
608
+ const task = readTask(db, id);
609
+ if (!task) throw new StateOpsError('TASK_NOT_FOUND', `no task ${id}`);
610
+ const lifecycleEvents = db.prepare(
611
+ 'SELECT type FROM events WHERE task_id = ? ORDER BY id',
612
+ ).all(id).map((row) => row.type).filter((type) => (
613
+ [
614
+ 'task_started',
615
+ 'task_completed',
616
+ 'task_blocked',
617
+ 'task_reopened',
618
+ 'task_stale_marked',
619
+ 'task_expanded',
620
+ ].includes(type)
621
+ || type.startsWith('session_')
622
+ || type.startsWith('worker_packet_')
623
+ || type.startsWith('task_failure')
624
+ || type.startsWith('task_circuit')
625
+ || type.includes('checkpoint')
626
+ || type.startsWith('workflow_')
627
+ ));
628
+ const references = [
629
+ ['sessions', db.prepare(
630
+ 'SELECT COUNT(*) AS count FROM sessions WHERE task_id = ?',
631
+ ).get(id).count],
632
+ ['children', db.prepare(
633
+ 'SELECT COUNT(*) AS count FROM tasks WHERE parent_id = ?',
634
+ ).get(id).count],
635
+ ['dependencies', db.prepare(
636
+ `SELECT COUNT(*) AS count FROM tasks
637
+ WHERE deps IS NOT NULL AND json_valid(deps)
638
+ AND EXISTS (SELECT 1 FROM json_each(tasks.deps) WHERE value = ?)`,
639
+ ).get(id).count],
640
+ ['worker_packets', db.prepare(
641
+ `SELECT COUNT(*) AS count FROM worker_packets
642
+ WHERE scope_type = 'task' AND scope_id = ?`,
643
+ ).get(id).count],
644
+ ['artifacts', db.prepare(
645
+ `SELECT COUNT(*) AS count FROM artifacts
646
+ WHERE task_id = ? OR (owner_type = 'task' AND owner_id = ?)`,
647
+ ).get(id, id).count],
648
+ ['artifact_edges', db.prepare(
649
+ `SELECT COUNT(*) AS count FROM artifact_edges
650
+ WHERE (source_type = 'task' AND source_id = ?)
651
+ OR (target_type = 'task' AND target_id = ?)`,
652
+ ).get(id, id).count],
653
+ ['stage_checkpoints', db.prepare(
654
+ `SELECT COUNT(*) AS count FROM stage_checkpoints
655
+ WHERE scope_type = 'task' AND scope_id = ?`,
656
+ ).get(id).count],
657
+ ['context_envelopes', db.prepare(
658
+ `SELECT COUNT(*) AS count FROM context_envelopes
659
+ WHERE scope_type = 'task' AND scope_id = ?`,
660
+ ).get(id).count],
661
+ ['workflow_runs', db.prepare(
662
+ 'SELECT COUNT(*) AS count FROM workflow_runs WHERE task_id = ?',
663
+ ).get(id).count],
664
+ ['context_snapshots', db.prepare(
665
+ 'SELECT COUNT(*) AS count FROM context_snapshots WHERE task_id = ?',
666
+ ).get(id).count],
667
+ ['incidents', db.prepare(
668
+ 'SELECT COUNT(*) AS count FROM incidents WHERE task_id = ?',
669
+ ).get(id).count],
670
+ ['spec_learning_candidates', db.prepare(
671
+ 'SELECT COUNT(*) AS count FROM spec_learning_candidates WHERE task_id = ?',
672
+ ).get(id).count],
673
+ ['trace_links', db.prepare(
674
+ 'SELECT COUNT(*) AS count FROM trace_links WHERE task_id = ?',
675
+ ).get(id).count],
676
+ ['circuit_breaker', db.prepare(
677
+ 'SELECT COUNT(*) AS count FROM circuit_breaker WHERE task_id = ?',
678
+ ).get(id).count],
679
+ ['session_close_journal', rootDir && closeJournal.findForTask(rootDir, id) ? 1 : 0],
680
+ ].filter(([, count]) => count > 0)
681
+ .map(([kind, count]) => ({ kind, count }));
682
+ if (references.length > 0) {
683
+ throw new StateOpsError(
684
+ 'TASK_DELETE_REFERENCED',
685
+ `task ${id} cannot be deleted because durable authority still references it`,
686
+ { details: { task_id: id, references } },
687
+ );
688
+ }
689
+ if (!force && (
690
+ task.status !== 'pending' || task.session_id || task.completion_commit
691
+ || lifecycleEvents.length > 0
692
+ )) {
693
+ throw new StateOpsError(
694
+ 'TASK_DELETE_NOT_DRAFT',
695
+ `task ${id} is not an unowned pending draft`,
696
+ {
697
+ details: {
698
+ task_id: id,
699
+ status: task.status,
700
+ session_id: task.session_id,
701
+ completion_commit: task.completion_commit,
702
+ lifecycle_events: [...new Set(lifecycleEvents)],
703
+ },
704
+ },
705
+ );
606
706
  }
607
707
  db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
608
- return { ok: true };
708
+ const contract = Object.fromEntries(
709
+ TASK_CONTRACT_DEFINE_FIELDS
710
+ .filter((field) => task[field] !== undefined && task[field] !== null)
711
+ .map((field) => [field, task[field]]),
712
+ );
713
+ const tombstone = appendEventInTx(db, {
714
+ type: 'task_deleted',
715
+ task_id: id,
716
+ change_id: task.change_id,
717
+ payload: {
718
+ id: task.id,
719
+ title: task.title,
720
+ type: task.type,
721
+ priority: task.priority,
722
+ reason: 'task_contract_remove',
723
+ contract_digest: canonical.digest(contract),
724
+ },
725
+ });
726
+ return { ok: true, tombstone };
609
727
  });
610
728
  }
611
729
 
612
730
  // ─── events ──────────────────────────────────────────────────────────────
613
731
 
732
+ const EVENT_FIELDS = new Set([
733
+ 'type', 'task_id', 'change_id', 'session_id', 'runtime', 'payload',
734
+ ]);
735
+
736
+ function eventText(value, field, { nullable = false } = {}) {
737
+ if (value === null && nullable) return null;
738
+ if (typeof value !== 'string' || value.trim() === '') {
739
+ throw new StateOpsError(
740
+ 'VALIDATION_ERROR',
741
+ `${field} must be a non-empty string${nullable ? ' or null' : ''}`,
742
+ );
743
+ }
744
+ return value.trim();
745
+ }
746
+
747
+ function assertJsonValue(value, field, seen = new Set()) {
748
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
749
+ if (typeof value === 'number') {
750
+ if (Number.isFinite(value)) return;
751
+ throw new StateOpsError('VALIDATION_ERROR', `${field} must contain valid JSON values`);
752
+ }
753
+ if (typeof value !== 'object') {
754
+ throw new StateOpsError('VALIDATION_ERROR', `${field} must contain valid JSON values`);
755
+ }
756
+ if (seen.has(value)) {
757
+ throw new StateOpsError('VALIDATION_ERROR', `${field} must not contain circular references`);
758
+ }
759
+ if (!Array.isArray(value)) {
760
+ const prototype = Object.getPrototypeOf(value);
761
+ if (prototype !== Object.prototype && prototype !== null) {
762
+ throw new StateOpsError('VALIDATION_ERROR', `${field} must contain only JSON objects`);
763
+ }
764
+ }
765
+ seen.add(value);
766
+ if (Array.isArray(value)) {
767
+ value.forEach((item, index) => assertJsonValue(item, `${field}[${index}]`, seen));
768
+ } else {
769
+ Object.entries(value).forEach(([key, item]) => (
770
+ assertJsonValue(item, `${field}.${key}`, seen)
771
+ ));
772
+ }
773
+ seen.delete(value);
774
+ }
775
+
614
776
  function appendEventInTx(db, event) {
615
- if (!event || !event.type) {
616
- throw new StateOpsError('VALIDATION_ERROR', 'event.type is required');
777
+ if (!event || typeof event !== 'object' || Array.isArray(event)) {
778
+ throw new StateOpsError('VALIDATION_ERROR', 'event must be an object');
779
+ }
780
+ const unknown = Object.keys(event).filter((field) => !EVENT_FIELDS.has(field));
781
+ if (unknown.length > 0) {
782
+ throw new StateOpsError('VALIDATION_ERROR', `event.${unknown[0]} is not allowed`);
617
783
  }
618
- if (event.runtime != null && !isSupportedRuntime(event.runtime)) {
619
- throw new StateOpsError('VALIDATION_ERROR', `unsupported runtime: ${event.runtime}`);
784
+ const type = eventText(event.type, 'event.type');
785
+ const taskId = event.task_id === undefined
786
+ ? null : eventText(event.task_id, 'event.task_id', { nullable: true });
787
+ const changeId = event.change_id === undefined
788
+ ? null : eventText(event.change_id, 'event.change_id', { nullable: true });
789
+ const sessionId = event.session_id === undefined
790
+ ? null : eventText(event.session_id, 'event.session_id', { nullable: true });
791
+ const runtime = event.runtime === undefined
792
+ ? null : eventText(event.runtime, 'event.runtime', { nullable: true });
793
+ if (runtime !== null && !isSupportedRuntime(runtime)) {
794
+ throw new StateOpsError('VALIDATION_ERROR', `unsupported runtime: ${runtime}`);
795
+ }
796
+ let payloadJson = null;
797
+ if (event.payload !== undefined) {
798
+ if (!event.payload || typeof event.payload !== 'object' || Array.isArray(event.payload)) {
799
+ throw new StateOpsError('VALIDATION_ERROR', 'event.payload must be a JSON object');
800
+ }
801
+ assertJsonValue(event.payload, 'event.payload');
802
+ payloadJson = JSON.stringify(event.payload);
620
803
  }
621
804
  const result = db.prepare(
622
805
  `INSERT INTO events (type, task_id, change_id, session_id, runtime, payload_json)
623
806
  VALUES (?, ?, ?, ?, ?, ?)`,
624
807
  ).run(
625
- event.type,
626
- event.task_id ?? null,
627
- event.change_id ?? null,
628
- event.session_id ?? null,
629
- event.runtime ?? null,
630
- event.payload === undefined ? null : JSON.stringify(event.payload),
808
+ type,
809
+ taskId,
810
+ changeId,
811
+ sessionId,
812
+ runtime,
813
+ payloadJson,
631
814
  );
632
815
  const row = db.prepare('SELECT id, ts FROM events WHERE id = ?').get(result.lastInsertRowid);
633
816
  return { event_id: Number(row.id), ts: row.ts };