ultra-builder-pro-cli 0.24.2 → 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.
- package/CHANGELOG.md +42 -0
- package/adapters/claude.js +104 -0
- package/adapters/codex.js +363 -40
- package/adapters/grok.js +1254 -58
- package/adapters/kimi.js +682 -13
- package/adapters/opencode.js +132 -11
- package/docs/AGENT-CONTEXT.md +14 -0
- package/docs/ARTIFACT-AUTHORITY.md +13 -0
- package/docs/COMMIT-HASH-BACKFILL.md +25 -18
- package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
- package/mcp-server/lib/artifact-registry.cjs +181 -51
- package/mcp-server/lib/baseline-workflow.cjs +160 -23
- package/mcp-server/lib/change-workflow.cjs +21 -16
- package/mcp-server/lib/context-envelope.cjs +124 -51
- package/mcp-server/lib/decision-records.cjs +164 -78
- package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
- package/mcp-server/lib/legacy-change-workflow.cjs +294 -71
- package/mcp-server/lib/managed-file-write.cjs +56 -0
- package/mcp-server/lib/prd-parser.cjs +2 -2
- package/mcp-server/lib/stage-checkpoints.cjs +163 -9
- package/mcp-server/lib/state-db.cjs +186 -8
- package/mcp-server/lib/state-ops.cjs +214 -31
- package/mcp-server/lib/task-contract.cjs +404 -0
- package/mcp-server/lib/task-expander.cjs +2 -2
- package/mcp-server/lib/task-ledger.cjs +175 -62
- package/mcp-server/lib/ultra-facade.cjs +1786 -169
- package/mcp-server/lib/worker-packet.cjs +2 -2
- package/mcp-server/server.cjs +23 -4
- package/orchestrator/parallel-orchestrator.cjs +114 -27
- package/orchestrator/session-runner.cjs +25 -136
- package/package.json +1 -1
- package/skills/ultra-change/SKILL.md +6 -1
- package/skills/ultra-dev/SKILL.md +9 -4
- package/skills/ultra-plan/SKILL.md +13 -4
- package/skills/ultra-plan/references/semantic-preflight.md +6 -4
- package/spec/mcp-tools.yaml +114 -9
- package/spec/schemas/context-file.v4.5.schema.json +2 -2
- package/spec/schemas/state-db.sql +10 -6
- package/spec/schemas/task-ledger.v2.schema.json +23 -8
- 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
|
-
|
|
39
|
-
'session_id', 'stale', 'tag',
|
|
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
|
|
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
|
|
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 (!
|
|
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(
|
|
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
|
|
603
|
-
if (!
|
|
604
|
-
|
|
605
|
-
|
|
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
|
-
|
|
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 ||
|
|
616
|
-
throw new StateOpsError('VALIDATION_ERROR', 'event
|
|
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
|
-
|
|
619
|
-
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
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 };
|