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.
Files changed (40) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/adapters/claude.js +104 -0
  3. package/adapters/codex.js +363 -40
  4. package/adapters/grok.js +1254 -58
  5. package/adapters/kimi.js +682 -13
  6. package/adapters/opencode.js +132 -11
  7. package/docs/AGENT-CONTEXT.md +14 -0
  8. package/docs/ARTIFACT-AUTHORITY.md +13 -0
  9. package/docs/COMMIT-HASH-BACKFILL.md +25 -18
  10. package/docs/RUNTIME-COMPAT-MATRIX.md +18 -2
  11. package/mcp-server/lib/artifact-registry.cjs +181 -51
  12. package/mcp-server/lib/baseline-workflow.cjs +160 -23
  13. package/mcp-server/lib/change-workflow.cjs +21 -16
  14. package/mcp-server/lib/context-envelope.cjs +124 -51
  15. package/mcp-server/lib/decision-records.cjs +164 -78
  16. package/mcp-server/lib/kernel-change-workflow.cjs +2029 -0
  17. package/mcp-server/lib/legacy-change-workflow.cjs +294 -71
  18. package/mcp-server/lib/managed-file-write.cjs +56 -0
  19. package/mcp-server/lib/prd-parser.cjs +2 -2
  20. package/mcp-server/lib/stage-checkpoints.cjs +163 -9
  21. package/mcp-server/lib/state-db.cjs +186 -8
  22. package/mcp-server/lib/state-ops.cjs +214 -31
  23. package/mcp-server/lib/task-contract.cjs +404 -0
  24. package/mcp-server/lib/task-expander.cjs +2 -2
  25. package/mcp-server/lib/task-ledger.cjs +175 -62
  26. package/mcp-server/lib/ultra-facade.cjs +1786 -169
  27. package/mcp-server/lib/worker-packet.cjs +2 -2
  28. package/mcp-server/server.cjs +23 -4
  29. package/orchestrator/parallel-orchestrator.cjs +114 -27
  30. package/orchestrator/session-runner.cjs +25 -136
  31. package/package.json +1 -1
  32. package/skills/ultra-change/SKILL.md +6 -1
  33. package/skills/ultra-dev/SKILL.md +9 -4
  34. package/skills/ultra-plan/SKILL.md +13 -4
  35. package/skills/ultra-plan/references/semantic-preflight.md +6 -4
  36. package/spec/mcp-tools.yaml +114 -9
  37. package/spec/schemas/context-file.v4.5.schema.json +2 -2
  38. package/spec/schemas/state-db.sql +10 -6
  39. package/spec/schemas/task-ledger.v2.schema.json +23 -8
  40. package/spec/schemas/tasks.v4.5.schema.json +34 -12
@@ -3,9 +3,18 @@
3
3
  const canonical = require('./canonical-json.cjs');
4
4
  const artifactRegistry = require('./artifact-registry.cjs');
5
5
  const ops = require('./state-ops.cjs');
6
- const { writeManagedJson } = require('./managed-file-write.cjs');
6
+ const {
7
+ restoreManagedFile,
8
+ snapshotManagedFile,
9
+ writeManagedJson,
10
+ } = require('./managed-file-write.cjs');
7
11
  const { readStableProjectFile } = require('./safe-project-file.cjs');
8
12
 
13
+ const DECISION_FIELDS = new Set([
14
+ 'id', 'scope', 'question', 'recommendation', 'selection', 'effects',
15
+ 'non_goals', 'owner', 'source', 'provenance', 'applied_refs', 'supersedes_id',
16
+ ]);
17
+
9
18
  class DecisionRecordError extends Error {
10
19
  constructor(code, message, details) {
11
20
  super(message);
@@ -21,11 +30,55 @@ function requiredText(value, field) {
21
30
  return normalized;
22
31
  }
23
32
 
33
+ function assertExactFields(value, allowed, field) {
34
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
35
+ throw new DecisionRecordError('VALIDATION_ERROR', `${field} must be an object`);
36
+ }
37
+ const unknown = Object.keys(value).filter((key) => !allowed.has(key));
38
+ if (unknown.length > 0) {
39
+ throw new DecisionRecordError(
40
+ 'VALIDATION_ERROR', `${field}.${unknown[0]} is not allowed`,
41
+ );
42
+ }
43
+ }
44
+
45
+ function assertJsonValue(value, field, seen = new Set()) {
46
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
47
+ if (typeof value === 'number') {
48
+ if (Number.isFinite(value)) return;
49
+ throw new DecisionRecordError('VALIDATION_ERROR', `${field} must contain valid JSON values`);
50
+ }
51
+ if (typeof value !== 'object') {
52
+ throw new DecisionRecordError('VALIDATION_ERROR', `${field} must contain valid JSON values`);
53
+ }
54
+ if (seen.has(value)) {
55
+ throw new DecisionRecordError('VALIDATION_ERROR', `${field} must not contain circular references`);
56
+ }
57
+ if (!Array.isArray(value)) {
58
+ const prototype = Object.getPrototypeOf(value);
59
+ if (prototype !== Object.prototype && prototype !== null) {
60
+ throw new DecisionRecordError(
61
+ 'VALIDATION_ERROR', `${field} must contain only JSON objects`,
62
+ );
63
+ }
64
+ }
65
+ seen.add(value);
66
+ if (Array.isArray(value)) {
67
+ value.forEach((item, index) => assertJsonValue(item, `${field}[${index}]`, seen));
68
+ } else {
69
+ Object.entries(value).forEach(([key, item]) => (
70
+ assertJsonValue(item, `${field}.${key}`, seen)
71
+ ));
72
+ }
73
+ seen.delete(value);
74
+ }
75
+
24
76
  function plainObject(value, field) {
25
77
  if (value === undefined) return {};
26
78
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
27
79
  throw new DecisionRecordError('VALIDATION_ERROR', `${field} must be an object`);
28
80
  }
81
+ assertJsonValue(value, field);
29
82
  return JSON.parse(JSON.stringify(value));
30
83
  }
31
84
 
@@ -38,6 +91,7 @@ function stringArray(value, field) {
38
91
  }
39
92
 
40
93
  function normalizeScope(scope = {}) {
94
+ assertExactFields(scope, new Set(['baseline_id', 'change_id']), 'scope');
41
95
  const baseline = typeof scope.baseline_id === 'string' ? scope.baseline_id.trim() : '';
42
96
  const change = typeof scope.change_id === 'string' ? scope.change_id.trim() : '';
43
97
  if (Boolean(baseline) === Boolean(change)) {
@@ -186,10 +240,19 @@ function normalizeAppliedRefs(value) {
186
240
  if (!item || typeof item !== 'object' || Array.isArray(item)) {
187
241
  throw new DecisionRecordError('VALIDATION_ERROR', `applied_refs[${index}] must be an object`);
188
242
  }
243
+ assertExactFields(
244
+ item,
245
+ new Set(['ref', 'field', 'digest']),
246
+ `applied_refs[${index}]`,
247
+ );
189
248
  return {
190
249
  ref: requiredText(item.ref, `applied_refs[${index}].ref`),
191
- ...(item.field ? { field: requiredText(item.field, `applied_refs[${index}].field`) } : {}),
192
- ...(item.digest ? { digest: requiredText(item.digest, `applied_refs[${index}].digest`) } : {}),
250
+ ...(item.field !== undefined
251
+ ? { field: requiredText(item.field, `applied_refs[${index}].field`) }
252
+ : {}),
253
+ ...(item.digest !== undefined
254
+ ? { digest: requiredText(item.digest, `applied_refs[${index}].digest`) }
255
+ : {}),
193
256
  };
194
257
  });
195
258
  }
@@ -201,7 +264,11 @@ function artifactPath(scope, id) {
201
264
  }
202
265
 
203
266
  function acceptDecision(db, input = {}, { rootDir = process.cwd() } = {}) {
267
+ assertExactFields(input, DECISION_FIELDS, 'decision');
204
268
  const scope = normalizeScope(input.scope);
269
+ const supersedesId = input.supersedes_id === undefined || input.supersedes_id === null
270
+ ? null
271
+ : requiredText(input.supersedes_id, 'supersedes_id');
205
272
  const value = {
206
273
  schema_version: '1.0',
207
274
  id: requiredText(input.id, 'id'),
@@ -216,9 +283,7 @@ function acceptDecision(db, input = {}, { rootDir = process.cwd() } = {}) {
216
283
  provenance: plainObject(input.provenance, 'provenance'),
217
284
  applied_refs: normalizeAppliedRefs(input.applied_refs),
218
285
  status: 'accepted',
219
- supersedes_id: input.supersedes_id
220
- ? requiredText(input.supersedes_id, 'supersedes_id')
221
- : null,
286
+ supersedes_id: supersedesId,
222
287
  };
223
288
  const digest = canonical.digest(value);
224
289
  const document = { ...value, digest };
@@ -247,83 +312,104 @@ function acceptDecision(db, input = {}, { rootDir = process.cwd() } = {}) {
247
312
  'a decision can only supersede another decision in the same scope',
248
313
  );
249
314
  }
315
+ if (prior.status !== 'accepted') {
316
+ throw new DecisionRecordError(
317
+ 'DECISION_NOT_SUPERSEDEABLE',
318
+ `decision ${value.supersedes_id} is ${prior.status}`,
319
+ );
320
+ }
250
321
  }
322
+ artifactRegistry.assertArtifactOwner(db, scope.type, scope.id);
323
+ const snapshot = snapshotManagedFile(rootDir, relative);
251
324
  const published = writeManagedJson(rootDir, relative, document);
252
- return ops.tx(db, () => {
253
- if (value.supersedes_id) {
325
+ try {
326
+ return ops.tx(db, () => {
327
+ if (value.supersedes_id) {
328
+ db.prepare(
329
+ `UPDATE decision_records SET status = 'superseded',
330
+ updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
331
+ WHERE id = ? AND status = 'accepted'`,
332
+ ).run(value.supersedes_id);
333
+ }
254
334
  db.prepare(
255
- `UPDATE decision_records SET status = 'superseded',
256
- updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
257
- WHERE id = ? AND status = 'accepted'`,
258
- ).run(value.supersedes_id);
259
- }
260
- db.prepare(
261
- `INSERT INTO decision_records
262
- (id, scope_type, scope_id, question, recommendation, selection,
263
- effects_json, non_goals_json, owner, source, provenance_json,
264
- applied_refs_json, status, digest, artifact_path, supersedes_id)
265
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'accepted', ?, ?, ?)`,
266
- ).run(
267
- value.id,
268
- scope.type,
269
- scope.id,
270
- value.question,
271
- value.recommendation,
272
- value.selection,
273
- JSON.stringify(value.effects),
274
- JSON.stringify(value.non_goals),
275
- value.owner,
276
- value.source,
277
- JSON.stringify(value.provenance),
278
- JSON.stringify(value.applied_refs),
279
- digest,
280
- relative,
281
- value.supersedes_id,
282
- );
283
- artifactRegistry.recordArtifactInTx(db, {
284
- id: `artifact-decision-${value.id}`,
285
- owner_type: scope.type,
286
- owner_id: scope.id,
287
- change_id: scope.type === 'change' ? scope.id : null,
288
- kind: 'decision_record',
289
- path: relative,
290
- content_digest: published.digest,
291
- source_refs: [{
292
- type: scope.type,
293
- id: scope.id,
294
- relation: 'decided_for',
295
- }],
296
- consumer_refs: [{
297
- type: 'external',
298
- id: 'ultra-context-consumer',
299
- relation: 'consumed_by',
300
- }],
301
- provenance: {
302
- writer: 'decision-records',
303
- decision_digest: digest,
304
- owner: value.owner,
305
- source: value.source,
306
- },
307
- metadata: {
308
- decision_id: value.id,
309
- status: value.status,
310
- supersedes_id: value.supersedes_id,
311
- },
312
- }, { rootDir });
313
- ops.appendEventInTx(db, {
314
- type: 'decision_recorded',
315
- change_id: scope.type === 'change' ? scope.id : null,
316
- payload: {
317
- decision_id: value.id,
318
- scope_type: scope.type,
319
- scope_id: scope.id,
335
+ `INSERT INTO decision_records
336
+ (id, scope_type, scope_id, question, recommendation, selection,
337
+ effects_json, non_goals_json, owner, source, provenance_json,
338
+ applied_refs_json, status, digest, artifact_path, supersedes_id)
339
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'accepted', ?, ?, ?)`,
340
+ ).run(
341
+ value.id,
342
+ scope.type,
343
+ scope.id,
344
+ value.question,
345
+ value.recommendation,
346
+ value.selection,
347
+ JSON.stringify(value.effects),
348
+ JSON.stringify(value.non_goals),
349
+ value.owner,
350
+ value.source,
351
+ JSON.stringify(value.provenance),
352
+ JSON.stringify(value.applied_refs),
320
353
  digest,
321
- artifact_path: relative,
322
- supersedes_id: value.supersedes_id,
323
- },
354
+ relative,
355
+ value.supersedes_id,
356
+ );
357
+ artifactRegistry.recordArtifactInTx(db, {
358
+ id: `artifact-decision-${value.id}`,
359
+ owner_type: scope.type,
360
+ owner_id: scope.id,
361
+ change_id: scope.type === 'change' ? scope.id : null,
362
+ kind: 'decision_record',
363
+ path: relative,
364
+ content_digest: published.digest,
365
+ source_refs: [{
366
+ type: scope.type,
367
+ id: scope.id,
368
+ relation: 'decided_for',
369
+ }],
370
+ consumer_refs: [{
371
+ type: 'external',
372
+ id: 'ultra-context-consumer',
373
+ relation: 'consumed_by',
374
+ }],
375
+ provenance: {
376
+ writer: 'decision-records',
377
+ decision_digest: digest,
378
+ owner: value.owner,
379
+ source: value.source,
380
+ },
381
+ metadata: {
382
+ decision_id: value.id,
383
+ status: value.status,
384
+ supersedes_id: value.supersedes_id,
385
+ },
386
+ }, { rootDir });
387
+ ops.appendEventInTx(db, {
388
+ type: 'decision_recorded',
389
+ change_id: scope.type === 'change' ? scope.id : null,
390
+ payload: {
391
+ decision_id: value.id,
392
+ scope_type: scope.type,
393
+ scope_id: scope.id,
394
+ digest,
395
+ artifact_path: relative,
396
+ supersedes_id: value.supersedes_id,
397
+ },
398
+ });
399
+ return readDecision(db, value.id);
324
400
  });
325
- return readDecision(db, value.id);
326
- });
401
+ } catch (error) {
402
+ try {
403
+ restoreManagedFile(rootDir, snapshot);
404
+ } catch (rollbackError) {
405
+ throw new DecisionRecordError(
406
+ 'RECORD_FILE_ROLLBACK_REQUIRED',
407
+ `decision database mutation failed and managed file recovery also failed: ${relative}`,
408
+ { cause: error.message, rollback_error: rollbackError.message },
409
+ );
410
+ }
411
+ throw error;
412
+ }
327
413
  }
328
414
 
329
415
  module.exports = {