ultra-builder-pro-cli 0.22.0 → 0.24.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 (93) hide show
  1. package/.ultra-template/docs/research/README.md +4 -4
  2. package/CHANGELOG.md +83 -0
  3. package/README.md +68 -35
  4. package/adapters/_shared/codex-assets.cjs +121 -60
  5. package/adapters/_shared/interaction-contract.cjs +20 -7
  6. package/adapters/_shared/provenance.cjs +7 -2
  7. package/adapters/_shared/runtime-assets.cjs +4 -3
  8. package/adapters/_shared/runtime-native.cjs +132 -0
  9. package/adapters/claude.js +61 -31
  10. package/adapters/codex.js +178 -81
  11. package/adapters/grok.js +372 -0
  12. package/adapters/kimi.js +8 -4
  13. package/adapters/opencode.js +211 -33
  14. package/agents/code-reviewer.md +4 -2
  15. package/agents/debugger.md +4 -1
  16. package/agents/review-code.md +7 -3
  17. package/agents/review-comments.md +7 -4
  18. package/agents/review-coordinator.md +11 -7
  19. package/agents/review-design.md +8 -4
  20. package/agents/review-errors.md +8 -4
  21. package/agents/review-spec.md +8 -5
  22. package/agents/review-tests.md +7 -4
  23. package/agents/tdd-runner.md +5 -2
  24. package/bin/install.js +9 -4
  25. package/bin/orchestrator.js +4 -13
  26. package/docs/AGENT-CONTEXT.md +141 -231
  27. package/docs/ARCHITECTURE.md +203 -498
  28. package/docs/ARTIFACT-AUTHORITY.md +38 -35
  29. package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
  30. package/docs/ROADMAP.md +85 -110
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +139 -174
  32. package/docs/SKILL-AUTHORING.md +3 -3
  33. package/docs/STATE-DB-ACCESS-POLICY.md +25 -20
  34. package/docs/WORKFLOW-LIFECYCLE.md +174 -442
  35. package/hooks/README.md +11 -8
  36. package/hooks/active_task_context.py +16 -16
  37. package/hooks/adapters/grok.py +134 -0
  38. package/hooks/{context_spine.py → context_envelope.py} +26 -22
  39. package/hooks/health_check.py +19 -29
  40. package/hooks/pre_stop_check.py +22 -12
  41. package/hooks/subagent_tracker.py +1 -1
  42. package/hooks/workflow_checkpoint.py +14 -13
  43. package/hooks/workflow_context.py +11 -12
  44. package/hooks/workflow_resume.py +14 -13
  45. package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
  46. package/mcp-server/lib/artifact-registry.cjs +25 -1
  47. package/mcp-server/lib/baseline-workflow.cjs +36 -38
  48. package/mcp-server/lib/canonical-json.cjs +19 -0
  49. package/mcp-server/lib/change-workflow.cjs +637 -49
  50. package/mcp-server/lib/context-envelope.cjs +508 -0
  51. package/mcp-server/lib/decision-records.cjs +337 -0
  52. package/mcp-server/lib/doctor.cjs +91 -33
  53. package/mcp-server/lib/init-project.cjs +78 -39
  54. package/mcp-server/lib/managed-file-write.cjs +116 -0
  55. package/mcp-server/lib/plan-checkpoint.cjs +177 -0
  56. package/mcp-server/lib/project-context.cjs +178 -0
  57. package/mcp-server/lib/stage-checkpoints.cjs +309 -0
  58. package/mcp-server/lib/state-db.cjs +155 -24
  59. package/mcp-server/lib/task-ledger.cjs +522 -13
  60. package/mcp-server/lib/ultra-facade.cjs +834 -0
  61. package/mcp-server/lib/worker-packet.cjs +358 -0
  62. package/mcp-server/server.cjs +208 -1059
  63. package/orchestrator/daemon.cjs +27 -9
  64. package/orchestrator/parallel-orchestrator.cjs +111 -76
  65. package/orchestrator/session-runner.cjs +135 -34
  66. package/package.json +10 -3
  67. package/skills/ultra-change/SKILL.md +39 -98
  68. package/skills/ultra-deliver/SKILL.md +42 -65
  69. package/skills/ultra-dev/SKILL.md +52 -99
  70. package/skills/ultra-doctor/SKILL.md +35 -57
  71. package/skills/ultra-init/SKILL.md +48 -88
  72. package/skills/ultra-init/references/brownfield-adoption.md +11 -8
  73. package/skills/ultra-plan/SKILL.md +54 -132
  74. package/skills/ultra-research/SKILL.md +58 -112
  75. package/skills/ultra-review/SKILL.md +28 -46
  76. package/skills/ultra-review/references/unified-schema.md +2 -0
  77. package/skills/ultra-review/scripts/review_wait.py +8 -0
  78. package/skills/ultra-status/SKILL.md +22 -45
  79. package/skills/ultra-test/SKILL.md +27 -61
  80. package/skills/ultra-think/SKILL.md +29 -48
  81. package/skills/ultra-think/references/interaction-boundary.md +56 -0
  82. package/spec/cli-protocol.md +80 -187
  83. package/spec/mcp-tools.yaml +130 -2738
  84. package/spec/schemas/mcp-tools.schema.json +1 -1
  85. package/spec/schemas/state-db.sql +107 -4
  86. package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
  87. package/ultra-tools/commands/status.cjs +369 -243
  88. package/mcp-server/lib/context-spine.cjs +0 -1208
  89. package/mcp-server/lib/decision-dialogue.cjs +0 -1128
  90. package/mcp-server/lib/project-breadcrumb.cjs +0 -173
  91. package/mcp-server/lib/spec-learning.cjs +0 -284
  92. package/mcp-server/lib/workflow-state.cjs +0 -2965
  93. package/skills/ultra-think/references/decision-dialogue.md +0 -153
@@ -0,0 +1,508 @@
1
+ 'use strict';
2
+
3
+ const path = require('node:path');
4
+
5
+ const { version: PACKAGE_VERSION } = require('../../package.json');
6
+ const artifactRegistry = require('./artifact-registry.cjs');
7
+ const baselines = require('./baseline-workflow.cjs');
8
+ const canonical = require('./canonical-json.cjs');
9
+ const changes = require('./change-workflow.cjs');
10
+ const decisions = require('./decision-records.cjs');
11
+ const { writeManagedJson } = require('./managed-file-write.cjs');
12
+ const ops = require('./state-ops.cjs');
13
+ const { readStableProjectFile } = require('./safe-project-file.cjs');
14
+ const checkpoints = require('./stage-checkpoints.cjs');
15
+ const taskLedger = require('./task-ledger.cjs');
16
+
17
+ const SUMMARY_LIMIT = 16 * 1024;
18
+ const FULL_LIMIT = 64 * 1024;
19
+
20
+ class ContextEnvelopeError extends Error {
21
+ constructor(code, message, details) {
22
+ super(message);
23
+ this.name = 'ContextEnvelopeError';
24
+ this.code = code;
25
+ if (details !== undefined) this.details = details;
26
+ }
27
+ }
28
+
29
+ function parseJson(value, fallback) {
30
+ try { return value == null ? fallback : JSON.parse(value); }
31
+ catch { return fallback; }
32
+ }
33
+
34
+ function text(value, max) {
35
+ if (typeof value !== 'string') return value;
36
+ if (value.length <= max) return value;
37
+ return `${value.slice(0, Math.max(0, max - 32))}…[truncated ${value.length - max + 32} chars]`;
38
+ }
39
+
40
+ function compactValue(value, {
41
+ depth = 0,
42
+ maxDepth = 5,
43
+ textLimit = 2000,
44
+ arrayLimit = 50,
45
+ } = {}) {
46
+ if (typeof value === 'string') return text(value, textLimit);
47
+ if (value === null || value === undefined || typeof value !== 'object') return value;
48
+ if (depth >= maxDepth) return Array.isArray(value) ? `[${value.length} items]` : '[object]';
49
+ if (Array.isArray(value)) {
50
+ return value.slice(0, arrayLimit).map((item) => compactValue(item, {
51
+ depth: depth + 1, maxDepth, textLimit, arrayLimit,
52
+ }));
53
+ }
54
+ return Object.fromEntries(Object.entries(value).map(([key, child]) => [
55
+ key,
56
+ compactValue(child, { depth: depth + 1, maxDepth, textLimit, arrayLimit }),
57
+ ]));
58
+ }
59
+
60
+ function selectedChange(db, changeId) {
61
+ if (!changeId) return null;
62
+ return changes.readChange(db, changeId);
63
+ }
64
+
65
+ function selectedTask(db, taskId) {
66
+ if (!taskId) return null;
67
+ return ops.readTask(db, taskId);
68
+ }
69
+
70
+ function resolvedScope(db, input = {}) {
71
+ const requested = input.scope || {};
72
+ const task = selectedTask(db, requested.task_id);
73
+ const changeId = requested.change_id || task?.change_id || null;
74
+ const change = selectedChange(db, changeId);
75
+ const baseline = baselines.readBaseline(db);
76
+ if (task) return { type: 'task', id: task.id, baseline, change, task };
77
+ if (change) return { type: 'change', id: change.id, baseline, change, task: null };
78
+ if (!requested.task_id && !requested.change_id) {
79
+ const activeChangeRow = db.prepare(
80
+ `SELECT id FROM changes
81
+ WHERE status IN ('active', 'blocked', 'ready')
82
+ ORDER BY
83
+ CASE status WHEN 'active' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
84
+ updated_at DESC,
85
+ rowid DESC
86
+ LIMIT 1`,
87
+ ).get();
88
+ if (activeChangeRow) {
89
+ const activeChange = selectedChange(db, activeChangeRow.id);
90
+ const activeTaskRow = db.prepare(
91
+ `SELECT id FROM tasks
92
+ WHERE change_id = ? AND status IN ('in_progress', 'blocked', 'pending')
93
+ ORDER BY
94
+ CASE status WHEN 'in_progress' THEN 0 WHEN 'blocked' THEN 1 ELSE 2 END,
95
+ updated_at DESC,
96
+ rowid DESC
97
+ LIMIT 1`,
98
+ ).get(activeChangeRow.id);
99
+ const activeTask = activeTaskRow ? selectedTask(db, activeTaskRow.id) : null;
100
+ if (activeTask) {
101
+ return {
102
+ type: 'task',
103
+ id: activeTask.id,
104
+ baseline,
105
+ change: activeChange,
106
+ task: activeTask,
107
+ };
108
+ }
109
+ return {
110
+ type: 'change',
111
+ id: activeChange.id,
112
+ baseline,
113
+ change: activeChange,
114
+ task: null,
115
+ };
116
+ }
117
+ }
118
+ if (baseline) return { type: 'baseline', id: baseline.id, baseline, change: null, task: null };
119
+ return { type: 'project', id: 'project', baseline: null, change: null, task: null };
120
+ }
121
+
122
+ function acceptedDecisionRows(db, scope, rootDir) {
123
+ const rows = [];
124
+ if (scope.baseline) {
125
+ rows.push(...decisions.listAcceptedDecisions(
126
+ db,
127
+ { baseline_id: scope.baseline.id },
128
+ { limit: 200, rootDir, validateFiles: true },
129
+ ));
130
+ }
131
+ if (scope.change) {
132
+ rows.push(...decisions.listAcceptedDecisions(
133
+ db,
134
+ { change_id: scope.change.id },
135
+ { limit: 200, rootDir, validateFiles: true },
136
+ ));
137
+ }
138
+ return rows;
139
+ }
140
+
141
+ function checkpointRows(db, scope) {
142
+ const rows = [];
143
+ if (scope.change) {
144
+ rows.push(...checkpoints.listCheckpoints(
145
+ db,
146
+ { scope: { change_id: scope.change.id }, limit: 100 },
147
+ ).filter((item) => item.status !== 'superseded'));
148
+ }
149
+ if (scope.task) {
150
+ rows.push(...checkpoints.listCheckpoints(
151
+ db,
152
+ { scope: { task_id: scope.task.id }, limit: 100 },
153
+ ).filter((item) => item.status !== 'superseded'));
154
+ }
155
+ if (!scope.change && scope.baseline) {
156
+ rows.push(...checkpoints.listCheckpoints(
157
+ db,
158
+ { scope: { baseline_id: scope.baseline.id }, limit: 100 },
159
+ ).filter((item) => item.status !== 'superseded'));
160
+ }
161
+ return rows;
162
+ }
163
+
164
+ function artifactRows(db, scope) {
165
+ const clauses = [
166
+ "status <> 'archived'",
167
+ "kind NOT IN ('context_envelope', 'worker_packet')",
168
+ ];
169
+ const params = [];
170
+ if (scope.task) {
171
+ clauses.push('(task_id = ? OR change_id = ?)');
172
+ params.push(scope.task.id, scope.change?.id || '');
173
+ } else if (scope.change) {
174
+ clauses.push('change_id = ?');
175
+ params.push(scope.change.id);
176
+ } else if (scope.baseline) {
177
+ clauses.push("(owner_type = 'baseline' AND owner_id = ?)");
178
+ params.push(scope.baseline.id);
179
+ } else {
180
+ clauses.push("owner_type = 'project'");
181
+ }
182
+ return db.prepare(
183
+ `SELECT id, owner_type, owner_id, kind, path, digest, status, provenance_json
184
+ FROM artifacts WHERE ${clauses.join(' AND ')}
185
+ ORDER BY updated_at DESC, id ASC LIMIT 500`,
186
+ ).all(...params).map((row) => ({
187
+ id: row.id,
188
+ owner_type: row.owner_type,
189
+ owner_id: row.owner_id,
190
+ kind: row.kind,
191
+ ref: row.path,
192
+ digest: row.digest,
193
+ status: row.status,
194
+ provenance: parseJson(row.provenance_json, {}),
195
+ }));
196
+ }
197
+
198
+ function executionState(db, scope) {
199
+ if (!scope.task) return { session: null };
200
+ const session = db.prepare(
201
+ `SELECT sid, task_id, runtime, worktree_path, artifact_dir, status,
202
+ lease_expires_at, heartbeat_at, started_at
203
+ FROM sessions WHERE task_id = ?
204
+ ORDER BY started_at DESC, rowid DESC LIMIT 1`,
205
+ ).get(scope.task.id) || null;
206
+ const packet = db.prepare(
207
+ `SELECT id, role, context_envelope_id, context_digest, task_digest,
208
+ decision_digest, packet_digest, packet_path, output_path, created_at
209
+ FROM worker_packets WHERE scope_type = 'task' AND scope_id = ?
210
+ AND status = 'assigned'
211
+ ORDER BY created_at DESC, rowid DESC LIMIT 1`,
212
+ ).get(scope.task.id) || null;
213
+ return { session, worker_packet: packet };
214
+ }
215
+
216
+ function ledgerHealth(db, rootDir) {
217
+ try {
218
+ const result = taskLedger.inspectTaskLedger(db, { rootDir });
219
+ return {
220
+ status: result.status || 'available',
221
+ generation: result.generation ?? null,
222
+ state_digest: result.state_digest || null,
223
+ path: taskLedger.LEDGER_RELATIVE_PATH,
224
+ };
225
+ } catch (error) {
226
+ return {
227
+ status: 'migration_required',
228
+ generation: null,
229
+ state_digest: null,
230
+ path: taskLedger.LEDGER_RELATIVE_PATH,
231
+ migration: {
232
+ required: true,
233
+ code: error.code || 'TASK_LEDGER_INVALID',
234
+ action: 'ultra.sync migrate',
235
+ },
236
+ };
237
+ }
238
+ }
239
+
240
+ function authorityPayload(db, input, options) {
241
+ const rootDir = path.resolve(options.rootDir || process.cwd());
242
+ const stage = input.stage || 'project';
243
+ const scope = resolvedScope(db, input);
244
+ const baselineHealth = scope.baseline
245
+ ? baselines.inspectBaseline(db, { rootDir })
246
+ : { blockers: [], warnings: [] };
247
+ const checkpointList = checkpointRows(db, scope);
248
+ const diagnostics = {
249
+ warnings: (baselineHealth.warnings || []).map((code) => ({ code, severity: 'warning' })),
250
+ needs_attention: checkpointList.flatMap((item) => (
251
+ item.diagnostics.filter((diagnostic) => diagnostic.severity === 'needs_attention')
252
+ )),
253
+ hard_conflicts: checkpointList.flatMap((item) => (
254
+ item.diagnostics.filter((diagnostic) => diagnostic.severity === 'hard_conflict')
255
+ )),
256
+ };
257
+ for (const code of baselineHealth.blockers || []) {
258
+ diagnostics.needs_attention.push({ code, severity: 'needs_attention' });
259
+ }
260
+ const ledger = ledgerHealth(db, rootDir);
261
+ if (ledger.migration?.required) {
262
+ diagnostics.needs_attention.push({
263
+ code: ledger.migration.code,
264
+ severity: 'needs_attention',
265
+ action: ledger.migration.action,
266
+ });
267
+ }
268
+ return {
269
+ schema_version: '1.0',
270
+ project: {
271
+ root: '.',
272
+ runtime: options.runtime || 'unknown',
273
+ plugin_version: PACKAGE_VERSION,
274
+ health: diagnostics.hard_conflicts.length > 0 ? 'conflict' : 'available',
275
+ },
276
+ git: scope.baseline ? {
277
+ head: baselineHealth.current_repository_revision || scope.baseline.repository_revision || null,
278
+ branch: scope.baseline.repository_branch || null,
279
+ scoped_worktree_digest: scope.baseline.worktree_digest || null,
280
+ drift: {
281
+ metadata_only: (baselineHealth.warnings || []).includes('BASELINE_METADATA_ONLY_DRIFT'),
282
+ source_or_spec: (baselineHealth.blockers || []).some((code) => (
283
+ String(code).includes('SOURCE') || String(code).includes('SPEC')
284
+ )),
285
+ },
286
+ } : null,
287
+ baseline: scope.baseline,
288
+ change: scope.change,
289
+ task: scope.task,
290
+ decisions: acceptedDecisionRows(db, scope, rootDir),
291
+ checkpoints: checkpointList,
292
+ evidence: [
293
+ ...(scope.baseline?.evidence || []),
294
+ ...artifactRows(db, scope),
295
+ ],
296
+ execution: {
297
+ stage,
298
+ ...executionState(db, scope),
299
+ },
300
+ team_checkpoint: ledger,
301
+ diagnostics,
302
+ };
303
+ }
304
+
305
+ function presentation(authority, detail) {
306
+ const summary = detail !== 'full';
307
+ return compactValue(authority, {
308
+ maxDepth: summary ? 4 : 7,
309
+ textLimit: summary ? 500 : 4000,
310
+ arrayLimit: summary ? 12 : 100,
311
+ });
312
+ }
313
+
314
+ function fitEnvelope(authority, detail, digest) {
315
+ const limit = detail === 'full' ? FULL_LIMIT : SUMMARY_LIMIT;
316
+ let envelope = presentation(authority, detail);
317
+ let output = { schema_version: '1.0', digest, detail, envelope };
318
+ if (Buffer.byteLength(JSON.stringify(output)) <= limit) return output;
319
+ envelope = {
320
+ ...envelope,
321
+ evidence: (envelope.evidence || []).slice(0, detail === 'full' ? 30 : 6),
322
+ decisions: (envelope.decisions || []).slice(0, detail === 'full' ? 30 : 8),
323
+ checkpoints: (envelope.checkpoints || []).slice(0, detail === 'full' ? 20 : 8),
324
+ cursors: {
325
+ evidence: authority.evidence.length > (detail === 'full' ? 30 : 6)
326
+ ? { available: authority.evidence.length, action: 'ultra.context full with narrower scope' }
327
+ : null,
328
+ decisions: authority.decisions.length > (detail === 'full' ? 30 : 8)
329
+ ? { available: authority.decisions.length, action: 'ultra.context full with narrower scope' }
330
+ : null,
331
+ },
332
+ };
333
+ output = { schema_version: '1.0', digest, detail, envelope };
334
+ if (Buffer.byteLength(JSON.stringify(output)) <= limit) return output;
335
+ envelope = compactValue(envelope, {
336
+ maxDepth: 4,
337
+ textLimit: detail === 'full' ? 1000 : 240,
338
+ arrayLimit: detail === 'full' ? 20 : 6,
339
+ });
340
+ output = { schema_version: '1.0', digest, detail, envelope };
341
+ if (Buffer.byteLength(JSON.stringify(output)) > limit) {
342
+ const error = new Error(`context envelope exceeds ${limit} byte limit after compaction`);
343
+ error.code = 'CONTEXT_ENVELOPE_LIMIT_EXCEEDED';
344
+ throw error;
345
+ }
346
+ return output;
347
+ }
348
+
349
+ function buildEnvelope(db, input = {}, options = {}) {
350
+ const authority = authorityPayload(db, input, options);
351
+ const digest = canonical.digest(authority);
352
+ const output = fitEnvelope(authority, input.detail || 'summary', digest);
353
+ output.bytes = Buffer.byteLength(JSON.stringify(output));
354
+ return output;
355
+ }
356
+
357
+ function contextArtifactPath(scope, id) {
358
+ if (scope.task) return `.ultra/.runtime/projections/contexts/${id}.json`;
359
+ if (scope.change) return `${scope.change.artifact_root}/contexts/${id}.json`;
360
+ return `.ultra/.runtime/projections/contexts/${id}.json`;
361
+ }
362
+
363
+ function readEnvelope(db, id, { rootDir = process.cwd() } = {}) {
364
+ const row = db.prepare('SELECT * FROM context_envelopes WHERE id = ?').get(id);
365
+ if (!row) {
366
+ throw new ContextEnvelopeError(
367
+ 'CONTEXT_ENVELOPE_NOT_FOUND',
368
+ `Context Envelope not found: ${id}`,
369
+ );
370
+ }
371
+ const read = readStableProjectFile(rootDir, row.artifact_path, { encoding: 'utf8' });
372
+ if (!row.file_digest || read.digest !== row.file_digest) {
373
+ throw new ContextEnvelopeError(
374
+ 'CONTEXT_ENVELOPE_FILE_DRIFT',
375
+ `Context Envelope bytes no longer match ${id}`,
376
+ {
377
+ path: row.artifact_path,
378
+ expected: row.file_digest || null,
379
+ actual: read.digest,
380
+ },
381
+ );
382
+ }
383
+ let document;
384
+ try {
385
+ document = JSON.parse(read.text);
386
+ } catch (cause) {
387
+ throw new ContextEnvelopeError(
388
+ 'CONTEXT_ENVELOPE_INVALID',
389
+ `Context Envelope is not valid JSON: ${row.artifact_path}`,
390
+ { cause: cause.message },
391
+ );
392
+ }
393
+ const storedPayload = parseJson(row.payload_json, null);
394
+ if (!storedPayload
395
+ || document.schema_version !== '1.0'
396
+ || document.detail !== 'full'
397
+ || document.digest !== row.digest
398
+ || canonical.canonicalJson(document.envelope)
399
+ !== canonical.canonicalJson(storedPayload)) {
400
+ throw new ContextEnvelopeError(
401
+ 'CONTEXT_ENVELOPE_DIGEST_MISMATCH',
402
+ `Context Envelope authority does not match its database record: ${id}`,
403
+ { path: row.artifact_path },
404
+ );
405
+ }
406
+ return {
407
+ id: row.id,
408
+ stage: row.stage,
409
+ scope_type: row.scope_type,
410
+ scope_id: row.scope_id,
411
+ digest: row.digest,
412
+ file_digest: row.file_digest,
413
+ artifact_path: row.artifact_path,
414
+ document,
415
+ };
416
+ }
417
+
418
+ function persistEnvelope(db, input = {}, options = {}) {
419
+ const full = buildEnvelope(db, { ...input, detail: 'full' }, options);
420
+ const existing = db.prepare(
421
+ 'SELECT * FROM context_envelopes WHERE digest = ?',
422
+ ).get(full.digest);
423
+ if (existing) {
424
+ readEnvelope(db, existing.id, options);
425
+ return {
426
+ id: existing.id,
427
+ digest: existing.digest,
428
+ artifact_path: existing.artifact_path,
429
+ stage: existing.stage,
430
+ scope_type: existing.scope_type,
431
+ scope_id: existing.scope_id,
432
+ file_digest: existing.file_digest,
433
+ };
434
+ }
435
+ const scope = resolvedScope(db, input);
436
+ const id = `context-${full.digest.slice(0, 24)}`;
437
+ const artifactPath = contextArtifactPath(scope, id);
438
+ const published = writeManagedJson(options.rootDir || process.cwd(), artifactPath, full);
439
+ ops.tx(db, () => {
440
+ db.prepare(
441
+ `INSERT INTO context_envelopes
442
+ (id, stage, scope_type, scope_id, digest, file_digest, payload_json, artifact_path)
443
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
444
+ ).run(
445
+ id,
446
+ input.stage || 'project',
447
+ scope.type,
448
+ scope.id,
449
+ full.digest,
450
+ published.digest,
451
+ JSON.stringify(full.envelope),
452
+ artifactPath,
453
+ );
454
+ if (!artifactPath.startsWith('.ultra/.runtime/')) {
455
+ artifactRegistry.recordArtifactInTx(db, {
456
+ id: `artifact-${id}`,
457
+ owner_type: scope.change ? 'change' : (scope.baseline ? 'baseline' : 'project'),
458
+ owner_id: scope.change?.id || scope.baseline?.id || 'project',
459
+ kind: 'context_envelope',
460
+ path: artifactPath,
461
+ content_digest: published.digest,
462
+ source_refs: [
463
+ ...(scope.change
464
+ ? [{ type: 'change', id: scope.change.id, relation: 'compiled_from' }]
465
+ : []),
466
+ ...(scope.task
467
+ ? [{ type: 'task', id: scope.task.id, relation: 'compiled_from_task_contract' }]
468
+ : []),
469
+ ...(scope.baseline && !scope.change
470
+ ? [{ type: 'baseline', id: scope.baseline.id, relation: 'compiled_from' }]
471
+ : []),
472
+ ],
473
+ consumer_refs: [{
474
+ type: 'external',
475
+ id: 'ultra-context-consumer',
476
+ relation: 'consumed_by',
477
+ }],
478
+ provenance: {
479
+ writer: 'context-envelope',
480
+ envelope_digest: full.digest,
481
+ stage: input.stage || 'project',
482
+ },
483
+ metadata: {
484
+ envelope_id: id,
485
+ semantic_digest: full.digest,
486
+ },
487
+ }, { rootDir: options.rootDir || process.cwd() });
488
+ }
489
+ });
490
+ return {
491
+ id,
492
+ digest: full.digest,
493
+ artifact_path: artifactPath,
494
+ stage: input.stage || 'project',
495
+ scope_type: scope.type,
496
+ scope_id: scope.id,
497
+ file_digest: published.digest,
498
+ };
499
+ }
500
+
501
+ module.exports = {
502
+ ContextEnvelopeError,
503
+ SUMMARY_LIMIT,
504
+ FULL_LIMIT,
505
+ buildEnvelope,
506
+ persistEnvelope,
507
+ readEnvelope,
508
+ };