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
@@ -4,12 +4,25 @@ const crypto = require('node:crypto');
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
  const util = require('node:util');
7
+ const Ajv = require('ajv/dist/2020');
8
+ const addFormats = require('ajv-formats');
7
9
 
8
10
  const canonical = require('./canonical-json.cjs');
9
11
  const decisionRecords = require('./decision-records.cjs');
12
+ const {
13
+ restoreManagedFile,
14
+ snapshotManagedFile,
15
+ writeManagedFile,
16
+ } = require('./managed-file-write.cjs');
10
17
  const ops = require('./state-ops.cjs');
11
18
  const { readStableProjectFile } = require('./safe-project-file.cjs');
12
19
  const stageCheckpoints = require('./stage-checkpoints.cjs');
20
+ const taskContract = require('./task-contract.cjs');
21
+ const taskLedgerSchema = require('../../spec/schemas/task-ledger.v2.schema.json');
22
+
23
+ const taskLedgerAjv = new Ajv({ allErrors: true, strict: false });
24
+ addFormats(taskLedgerAjv);
25
+ const validateTaskLedgerSchema = taskLedgerAjv.compile(taskLedgerSchema);
13
26
 
14
27
  const LEDGER_KIND = 'ultra-team-task-ledger';
15
28
  const LEDGER_SCHEMA_VERSION = '2.0';
@@ -17,6 +30,7 @@ const LEGACY_LEDGER_SCHEMA_VERSION = '1.0';
17
30
  const LEDGER_RELATIVE_PATH = '.ultra/tasks/tasks.json';
18
31
  const HASH_PATTERN = /^[0-9a-f]{64}$/;
19
32
  const MAX_ANCESTORS = 64;
33
+ const PUBLICATION_ROLLBACK = Symbol('task-ledger-publication-rollback');
20
34
  const DURABLE_BASELINE_FIELDS = Object.freeze([
21
35
  'id',
22
36
  'project_name',
@@ -48,29 +62,7 @@ const LEGACY_DURABLE_BASELINE_FIELDS = Object.freeze(
48
62
  field === 'research_checkpoint_id' ? 'research_run_id' : field
49
63
  )),
50
64
  );
51
- const DURABLE_TASK_FIELDS = Object.freeze([
52
- 'id',
53
- 'title',
54
- 'type',
55
- 'priority',
56
- 'complexity',
57
- 'estimated_days',
58
- 'status',
59
- 'deps',
60
- 'files_modified',
61
- 'stale',
62
- 'trace_to',
63
- 'outcome',
64
- 'slice_kind',
65
- 'public_seam',
66
- 'verification_command',
67
- 'acceptance',
68
- 'context_refs',
69
- 'docs_impact',
70
- 'ownership',
71
- 'change_id',
72
- 'parent_id',
73
- ]);
65
+ const DURABLE_TASK_FIELDS = taskContract.DURABLE_TASK_FIELDS;
74
66
  const DURABLE_CHANGE_FIELDS = Object.freeze([
75
67
  'id',
76
68
  'title',
@@ -84,6 +76,7 @@ const DURABLE_CHANGE_FIELDS = Object.freeze([
84
76
  'classification',
85
77
  'research_disposition',
86
78
  'base_commit',
79
+ 'supersedes_id',
87
80
  'artifact_root',
88
81
  'closed_at',
89
82
  ]);
@@ -241,6 +234,7 @@ function portableChange(row) {
241
234
  ['classification', parseJson(row.classification_json, {})],
242
235
  ['research_disposition', parseJson(row.research_disposition_json, {})],
243
236
  ['base_commit', row.base_commit],
237
+ ['supersedes_id', row.supersedes_id],
244
238
  ['artifact_root', row.artifact_root],
245
239
  ['closed_at', row.closed_at],
246
240
  ]);
@@ -331,6 +325,20 @@ function durableStatus(task, previous) {
331
325
  return 'pending';
332
326
  }
333
327
 
328
+ function assertValidTaskContract(entry) {
329
+ try {
330
+ taskContract.validateDurableTask(entry, { metadata: false });
331
+ } catch (error) {
332
+ if (!(error instanceof taskContract.TaskContractError)) throw error;
333
+ throw new TaskLedgerError(
334
+ 'TASK_LEDGER_INVALID',
335
+ `task ledger entry ${entry?.id || '(unknown)'} violates the Task Contract: ${error.message}`,
336
+ error.details,
337
+ );
338
+ }
339
+ return entry;
340
+ }
341
+
334
342
  function durableTask(task, previous = null) {
335
343
  const value = compactObject([
336
344
  ['id', task.id],
@@ -340,29 +348,33 @@ function durableTask(task, previous = null) {
340
348
  ['complexity', task.complexity],
341
349
  ['estimated_days', task.estimated_days],
342
350
  ['status', durableStatus(task, previous)],
343
- ['deps', Array.isArray(task.deps) ? task.deps : []],
344
- ['files_modified', Array.isArray(task.files_modified) ? task.files_modified : []],
345
- ['stale', Boolean(task.stale)],
351
+ ['deps', task.deps == null ? [] : task.deps],
352
+ ['files_modified', task.files_modified == null ? [] : task.files_modified],
353
+ ['stale', task.stale == null ? false : task.stale],
346
354
  ['trace_to', task.trace_to],
347
355
  ['outcome', task.outcome],
348
356
  ['slice_kind', task.slice_kind],
349
357
  ['public_seam', task.public_seam],
350
358
  ['verification_command', task.verification_command],
351
- ['acceptance', Array.isArray(task.acceptance) ? task.acceptance : []],
352
- ['context_refs', Array.isArray(task.context_refs) ? task.context_refs : []],
353
- ['docs_impact', task.docs_impact || { status: 'unknown', files: [], rationale: null }],
354
- ['ownership', task.ownership || {}],
359
+ ['acceptance', task.acceptance == null ? [] : task.acceptance],
360
+ ['context_refs', task.context_refs == null ? [] : task.context_refs],
361
+ ['docs_impact', task.docs_impact == null
362
+ ? { status: 'unknown', files: [], rationale: null }
363
+ : task.docs_impact],
364
+ ['ownership', task.ownership == null ? {} : task.ownership],
355
365
  ['change_id', task.change_id],
356
366
  ['parent_id', task.parent_id],
357
367
  ]);
358
368
  const digest = sha256(value);
359
- if (previous && previous.digest === digest) return previous;
360
- return {
369
+ const entry = {
361
370
  ...value,
362
371
  revision: previous ? Number(previous.revision || 0) + 1 : 1,
363
372
  parent_digest: previous?.digest || null,
364
373
  digest,
365
374
  };
375
+ assertValidTaskContract(entry);
376
+ if (previous && previous.digest === digest) return previous;
377
+ return entry;
366
378
  }
367
379
 
368
380
  function statePayload({
@@ -385,7 +397,9 @@ function validateTask(entry) {
385
397
  if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
386
398
  throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every task ledger entry must be an object');
387
399
  }
388
- for (const field of ['id', 'title', 'type', 'priority', 'status', 'revision', 'digest']) {
400
+ for (const field of [
401
+ 'id', 'title', 'type', 'priority', 'status', 'revision', 'digest',
402
+ ]) {
389
403
  if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
390
404
  throw new TaskLedgerError(
391
405
  'TASK_LEDGER_INVALID',
@@ -393,6 +407,19 @@ function validateTask(entry) {
393
407
  );
394
408
  }
395
409
  }
410
+ if (!Object.prototype.hasOwnProperty.call(entry, 'parent_digest')) {
411
+ throw new TaskLedgerError(
412
+ 'TASK_LEDGER_INVALID',
413
+ `task ledger entry ${entry.id || '(unknown)'} is missing parent_digest`,
414
+ );
415
+ }
416
+ assertValidTaskContract(entry);
417
+ if (!Number.isInteger(entry.revision) || entry.revision < 1) {
418
+ throw new TaskLedgerError(
419
+ 'TASK_LEDGER_INVALID',
420
+ `task ledger entry ${entry.id} has an invalid revision`,
421
+ );
422
+ }
396
423
  if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(
397
424
  Object.fromEntries(DURABLE_TASK_FIELDS
398
425
  .filter((field) => entry[field] !== undefined)
@@ -411,6 +438,35 @@ function validateTask(entry) {
411
438
  }
412
439
  }
413
440
 
441
+ function validateLegacyTask(entry) {
442
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
443
+ throw new TaskLedgerError('TASK_LEDGER_INVALID', 'every legacy task entry must be an object');
444
+ }
445
+ for (const field of ['id', 'title', 'type', 'priority', 'status', 'revision', 'digest']) {
446
+ if (entry[field] === undefined || entry[field] === null || entry[field] === '') {
447
+ throw new TaskLedgerError(
448
+ 'TASK_LEDGER_INVALID',
449
+ `legacy task entry ${entry.id || '(unknown)'} is missing ${field}`,
450
+ );
451
+ }
452
+ }
453
+ const value = Object.fromEntries(DURABLE_TASK_FIELDS
454
+ .filter((field) => entry[field] !== undefined)
455
+ .map((field) => [field, entry[field]]));
456
+ if (!HASH_PATTERN.test(entry.digest) || entry.digest !== sha256(value)) {
457
+ throw new TaskLedgerError(
458
+ 'TASK_LEDGER_DIGEST_MISMATCH',
459
+ `legacy task entry ${entry.id} does not match its digest`,
460
+ );
461
+ }
462
+ if (entry.parent_digest != null && !HASH_PATTERN.test(entry.parent_digest)) {
463
+ throw new TaskLedgerError(
464
+ 'TASK_LEDGER_INVALID',
465
+ `legacy task entry ${entry.id} has an invalid parent_digest`,
466
+ );
467
+ }
468
+ }
469
+
414
470
  function validateBaseline(entry) {
415
471
  if (entry == null) return;
416
472
  if (typeof entry !== 'object' || Array.isArray(entry)) {
@@ -571,7 +627,7 @@ function validateLegacyLedger(document, file) {
571
627
  return false;
572
628
  }
573
629
  validateLegacyBaseline(document.baseline);
574
- document.tasks.forEach(validateTask);
630
+ document.tasks.forEach(validateLegacyTask);
575
631
  document.changes.forEach(validateChange);
576
632
  const expected = sha256({
577
633
  baseline: document.baseline || null,
@@ -615,6 +671,16 @@ function validateLedger(document, file = '(memory)') {
615
671
  `${file} is not an Ultra team task ledger v${LEDGER_SCHEMA_VERSION}`,
616
672
  );
617
673
  }
674
+ if (!validateTaskLedgerSchema(document)) {
675
+ const first = validateTaskLedgerSchema.errors?.[0];
676
+ const location = first?.instancePath || '(root)';
677
+ throw new TaskLedgerError(
678
+ 'TASK_LEDGER_INVALID',
679
+ `${file} violates the exact v${LEDGER_SCHEMA_VERSION} schema at ${location}: `
680
+ + `${first?.message || 'schema validation failed'}`,
681
+ { errors: validateTaskLedgerSchema.errors || [] },
682
+ );
683
+ }
618
684
  if (document.ancestors.length > MAX_ANCESTORS
619
685
  || document.ancestors.some((digest) => !HASH_PATTERN.test(digest))) {
620
686
  throw new TaskLedgerError(
@@ -703,22 +769,12 @@ function readTaskLedger(rootDir, { optional = true } = {}) {
703
769
  }
704
770
 
705
771
  function writeLedgerAtomic(rootDir, document) {
706
- const file = ledgerPath(rootDir);
707
- const dir = safeDirectoryChain(rootDir, ['.ultra', 'tasks'], { create: true });
708
- if (fs.existsSync(file)) {
709
- const stat = fs.lstatSync(file);
710
- if (stat.isSymbolicLink() || !stat.isFile()) {
711
- throw new TaskLedgerError('TASK_LEDGER_UNSAFE', `task ledger target is unsafe: ${file}`);
712
- }
713
- }
714
- const temp = `${file}.tmp-${process.pid}-${Date.now()}`;
715
- try {
716
- fs.writeFileSync(temp, `${JSON.stringify(document, null, 2)}\n`, { flag: 'wx' });
717
- fs.renameSync(temp, file);
718
- } finally {
719
- try { fs.rmSync(temp, { force: true }); } catch { /* best effort */ }
720
- }
721
- return file;
772
+ writeManagedFile(
773
+ rootDir,
774
+ LEDGER_RELATIVE_PATH,
775
+ `${JSON.stringify(document, null, 2)}\n`,
776
+ );
777
+ return ledgerPath(rootDir);
722
778
  }
723
779
 
724
780
  function migrateLegacyProjection(db, rootDir, originalError) {
@@ -998,19 +1054,56 @@ function publishTaskLedger(db, {
998
1054
  tasks,
999
1055
  };
1000
1056
  validateLedger(document);
1001
- const file = writeLedgerAtomic(rootDir, document);
1002
- recordLedgerEvent(db, 'task_ledger_published', document, {
1003
- reason,
1004
- migrated_legacy_projection: prior.migrated,
1005
- legacy_backup_path: prior.backup,
1006
- });
1007
- return {
1057
+ const snapshot = snapshotManagedFile(rootDir, LEDGER_RELATIVE_PATH);
1058
+ let file;
1059
+ try {
1060
+ file = writeLedgerAtomic(rootDir, document);
1061
+ recordLedgerEvent(db, 'task_ledger_published', document, {
1062
+ reason,
1063
+ migrated_legacy_projection: prior.migrated,
1064
+ legacy_backup_path: prior.backup,
1065
+ });
1066
+ } catch (error) {
1067
+ try {
1068
+ restoreManagedFile(rootDir, snapshot);
1069
+ } catch (rollbackError) {
1070
+ throw new TaskLedgerError(
1071
+ 'STATE_CORRUPT',
1072
+ 'task ledger publication failed and the previous ledger bytes could not be restored',
1073
+ {
1074
+ cause: error.code || error.message,
1075
+ rollback: rollbackError.code || rollbackError.message,
1076
+ path: LEDGER_RELATIVE_PATH,
1077
+ },
1078
+ );
1079
+ }
1080
+ throw error;
1081
+ }
1082
+ const result = {
1008
1083
  changed: true,
1009
1084
  path: file,
1010
1085
  ledger: document,
1011
1086
  migrated_legacy_projection: prior.migrated,
1012
1087
  legacy_backup_path: prior.backup,
1013
1088
  };
1089
+ Object.defineProperty(result, PUBLICATION_ROLLBACK, {
1090
+ configurable: false,
1091
+ enumerable: false,
1092
+ writable: false,
1093
+ value: { rootDir: path.resolve(rootDir), snapshot },
1094
+ });
1095
+ return result;
1096
+ }
1097
+
1098
+ function rollbackTaskLedgerPublication(publication) {
1099
+ const rollback = publication?.[PUBLICATION_ROLLBACK];
1100
+ if (!rollback) return { restored: false, reason: 'NO_PUBLICATION_ROLLBACK' };
1101
+ restoreManagedFile(rollback.rootDir, rollback.snapshot);
1102
+ return {
1103
+ restored: true,
1104
+ path: rollback.snapshot.path,
1105
+ previous_existed: rollback.snapshot.existed,
1106
+ };
1014
1107
  }
1015
1108
 
1016
1109
  function latestImportedCheckpoint(db) {
@@ -1247,8 +1340,8 @@ function insertChange(db, change) {
1247
1340
  `INSERT INTO changes
1248
1341
  (id, title, kind, status, intent, docs_impact_json, provider_refs_json,
1249
1342
  baseline_bypass_json, contract_json, classification_json,
1250
- research_disposition_json, base_commit, artifact_root, closed_at)
1251
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1343
+ research_disposition_json, base_commit, supersedes_id, artifact_root, closed_at)
1344
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
1252
1345
  ).run(
1253
1346
  change.id,
1254
1347
  change.title,
@@ -1262,6 +1355,7 @@ function insertChange(db, change) {
1262
1355
  JSON.stringify(change.classification || {}),
1263
1356
  JSON.stringify(change.research_disposition || {}),
1264
1357
  change.base_commit || null,
1358
+ change.supersedes_id || null,
1265
1359
  change.artifact_root,
1266
1360
  change.closed_at || null,
1267
1361
  );
@@ -1273,7 +1367,7 @@ function updateChangeFromLedger(db, change) {
1273
1367
  title = ?, kind = ?, status = ?, intent = ?, docs_impact_json = ?,
1274
1368
  provider_refs_json = ?, baseline_bypass_json = ?, contract_json = ?,
1275
1369
  classification_json = ?, research_disposition_json = ?, base_commit = ?,
1276
- artifact_root = ?, closed_at = ?, updated_at = ?
1370
+ supersedes_id = ?, artifact_root = ?, closed_at = ?, updated_at = ?
1277
1371
  WHERE id = ?`,
1278
1372
  ).run(
1279
1373
  change.title,
@@ -1287,6 +1381,7 @@ function updateChangeFromLedger(db, change) {
1287
1381
  JSON.stringify(change.classification || {}),
1288
1382
  JSON.stringify(change.research_disposition || {}),
1289
1383
  change.base_commit || null,
1384
+ change.supersedes_id || null,
1290
1385
  change.artifact_root,
1291
1386
  change.closed_at || null,
1292
1387
  new Date().toISOString(),
@@ -1317,7 +1412,9 @@ function insertTaskFromLedger(db, task) {
1317
1412
  change_id: task.change_id || null,
1318
1413
  parent_id: null,
1319
1414
  });
1320
- if (task.status !== created.status) updateTaskFromLedger(db, task);
1415
+ if (task.status !== created.status || Boolean(task.stale) !== created.stale) {
1416
+ updateTaskFromLedger(db, task);
1417
+ }
1321
1418
  return ops.readTask(db, task.id);
1322
1419
  }
1323
1420
 
@@ -1652,6 +1749,21 @@ function importTaskLedger(db, {
1652
1749
  parentUpdates.push([incoming.id, incoming.parent_id || null]);
1653
1750
  continue;
1654
1751
  }
1752
+ const runningSession = db.prepare(
1753
+ "SELECT sid FROM sessions WHERE task_id = ? AND status = 'running' LIMIT 1",
1754
+ ).get(incoming.id);
1755
+ if (current.session_id || runningSession) {
1756
+ throw new TaskLedgerError(
1757
+ 'TASK_LEDGER_ACTIVE_TASK_CONFLICT',
1758
+ `task ${incoming.id} changed in Git while a local session still owns it`,
1759
+ {
1760
+ task_id: incoming.id,
1761
+ local_status: current.status,
1762
+ task_session_id: current.session_id || null,
1763
+ running_session_id: runningSession?.sid || null,
1764
+ },
1765
+ );
1766
+ }
1655
1767
  if (current.status === 'in_progress' && last && incoming.digest !== last) {
1656
1768
  throw new TaskLedgerError(
1657
1769
  'TASK_LEDGER_ACTIVE_TASK_CONFLICT',
@@ -1717,7 +1829,7 @@ function importTaskLedger(db, {
1717
1829
  { task_id: id, session_count: sessionCount },
1718
1830
  );
1719
1831
  }
1720
- db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
1832
+ ops.deleteTask(db, id, { rootDir });
1721
1833
  deleted += 1;
1722
1834
  }
1723
1835
  for (const [id, parentId] of parentUpdates) {
@@ -1846,6 +1958,7 @@ module.exports = {
1846
1958
  ledgerPath,
1847
1959
  publishTaskLedger,
1848
1960
  readTaskLedger,
1961
+ rollbackTaskLedgerPublication,
1849
1962
  syncTaskLedger,
1850
1963
  validateLedger,
1851
1964
  };