ultra-builder-pro-cli 0.23.0 → 0.24.1

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 (95) hide show
  1. package/.ultra-template/docs/research/README.md +4 -4
  2. package/.ultra-template/tasks/tasks.json +4 -2
  3. package/CHANGELOG.md +72 -0
  4. package/README.md +55 -39
  5. package/adapters/_shared/codex-assets.cjs +121 -65
  6. package/adapters/_shared/interaction-contract.cjs +7 -1
  7. package/adapters/_shared/provenance.cjs +7 -2
  8. package/adapters/_shared/runtime-assets.cjs +3 -2
  9. package/adapters/_shared/runtime-native.cjs +132 -0
  10. package/adapters/claude.js +60 -30
  11. package/adapters/codex.js +178 -81
  12. package/adapters/grok.js +372 -0
  13. package/adapters/kimi.js +7 -3
  14. package/adapters/opencode.js +210 -32
  15. package/agents/code-reviewer.md +4 -2
  16. package/agents/debugger.md +4 -1
  17. package/agents/review-code.md +7 -3
  18. package/agents/review-comments.md +7 -4
  19. package/agents/review-coordinator.md +11 -7
  20. package/agents/review-design.md +8 -4
  21. package/agents/review-errors.md +8 -4
  22. package/agents/review-spec.md +8 -5
  23. package/agents/review-tests.md +7 -4
  24. package/agents/tdd-runner.md +5 -2
  25. package/bin/install.js +9 -4
  26. package/bin/orchestrator.js +4 -13
  27. package/docs/AGENT-CONTEXT.md +143 -229
  28. package/docs/ARCHITECTURE.md +205 -494
  29. package/docs/ARTIFACT-AUTHORITY.md +35 -28
  30. package/docs/LEGACY-CLI-CROSSWALK.md +1 -1
  31. package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
  32. package/docs/ROADMAP.md +85 -110
  33. package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
  34. package/docs/SKILL-AUTHORING.md +3 -3
  35. package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
  36. package/docs/WORKFLOW-LIFECYCLE.md +189 -448
  37. package/hooks/README.md +11 -8
  38. package/hooks/active_task_context.py +16 -16
  39. package/hooks/adapters/grok.py +134 -0
  40. package/hooks/{context_spine.py → context_envelope.py} +26 -22
  41. package/hooks/health_check.py +29 -34
  42. package/hooks/pre_stop_check.py +22 -12
  43. package/hooks/subagent_tracker.py +1 -1
  44. package/hooks/workflow_checkpoint.py +14 -13
  45. package/hooks/workflow_context.py +11 -12
  46. package/hooks/workflow_resume.py +14 -13
  47. package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
  48. package/mcp-server/lib/baseline-workflow.cjs +108 -82
  49. package/mcp-server/lib/canonical-json.cjs +19 -0
  50. package/mcp-server/lib/change-authority.cjs +26 -0
  51. package/mcp-server/lib/change-workflow.cjs +630 -45
  52. package/mcp-server/lib/context-envelope.cjs +508 -0
  53. package/mcp-server/lib/decision-records.cjs +337 -0
  54. package/mcp-server/lib/doctor.cjs +91 -33
  55. package/mcp-server/lib/init-project.cjs +78 -39
  56. package/mcp-server/lib/managed-file-write.cjs +116 -0
  57. package/mcp-server/lib/plan-checkpoint.cjs +177 -0
  58. package/mcp-server/lib/project-context.cjs +178 -0
  59. package/mcp-server/lib/stage-checkpoints.cjs +309 -0
  60. package/mcp-server/lib/state-db.cjs +155 -24
  61. package/mcp-server/lib/state-ops.cjs +21 -3
  62. package/mcp-server/lib/task-ledger.cjs +522 -13
  63. package/mcp-server/lib/ultra-facade.cjs +757 -442
  64. package/mcp-server/lib/worker-packet.cjs +358 -0
  65. package/mcp-server/server.cjs +200 -1103
  66. package/orchestrator/daemon.cjs +27 -9
  67. package/orchestrator/parallel-orchestrator.cjs +111 -76
  68. package/orchestrator/session-runner.cjs +135 -34
  69. package/package.json +10 -3
  70. package/skills/ultra-change/SKILL.md +10 -8
  71. package/skills/ultra-deliver/SKILL.md +17 -19
  72. package/skills/ultra-dev/SKILL.md +20 -11
  73. package/skills/ultra-doctor/SKILL.md +4 -4
  74. package/skills/ultra-init/SKILL.md +6 -5
  75. package/skills/ultra-init/references/brownfield-adoption.md +20 -14
  76. package/skills/ultra-plan/SKILL.md +13 -9
  77. package/skills/ultra-research/SKILL.md +18 -9
  78. package/skills/ultra-review/SKILL.md +14 -9
  79. package/skills/ultra-review/references/unified-schema.md +2 -0
  80. package/skills/ultra-review/scripts/review_wait.py +8 -0
  81. package/skills/ultra-status/SKILL.md +2 -2
  82. package/skills/ultra-test/SKILL.md +10 -6
  83. package/skills/ultra-think/SKILL.md +8 -7
  84. package/skills/ultra-think/references/interaction-boundary.md +56 -0
  85. package/spec/cli-protocol.md +80 -199
  86. package/spec/mcp-tools.yaml +43 -2806
  87. package/spec/schemas/state-db.sql +107 -4
  88. package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
  89. package/ultra-tools/commands/status.cjs +369 -243
  90. package/mcp-server/lib/context-spine.cjs +0 -1208
  91. package/mcp-server/lib/decision-dialogue.cjs +0 -1128
  92. package/mcp-server/lib/project-breadcrumb.cjs +0 -173
  93. package/mcp-server/lib/spec-learning.cjs +0 -284
  94. package/mcp-server/lib/workflow-state.cjs +0 -3129
  95. package/skills/ultra-think/references/decision-dialogue.md +0 -155
@@ -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
+ };