ultra-builder-pro-cli 0.24.2 → 0.25.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 (41) hide show
  1. package/CHANGELOG.md +51 -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/runtime-paths.cjs +43 -16
  21. package/mcp-server/lib/stage-checkpoints.cjs +163 -9
  22. package/mcp-server/lib/state-db.cjs +186 -8
  23. package/mcp-server/lib/state-ops.cjs +214 -31
  24. package/mcp-server/lib/task-contract.cjs +404 -0
  25. package/mcp-server/lib/task-expander.cjs +2 -2
  26. package/mcp-server/lib/task-ledger.cjs +175 -62
  27. package/mcp-server/lib/ultra-facade.cjs +1786 -169
  28. package/mcp-server/lib/worker-packet.cjs +2 -2
  29. package/mcp-server/server.cjs +23 -4
  30. package/orchestrator/parallel-orchestrator.cjs +114 -27
  31. package/orchestrator/session-runner.cjs +25 -136
  32. package/package.json +1 -1
  33. package/skills/ultra-change/SKILL.md +6 -1
  34. package/skills/ultra-dev/SKILL.md +9 -4
  35. package/skills/ultra-plan/SKILL.md +13 -4
  36. package/skills/ultra-plan/references/semantic-preflight.md +6 -4
  37. package/spec/mcp-tools.yaml +114 -9
  38. package/spec/schemas/context-file.v4.5.schema.json +2 -2
  39. package/spec/schemas/state-db.sql +10 -6
  40. package/spec/schemas/task-ledger.v2.schema.json +23 -8
  41. package/spec/schemas/tasks.v4.5.schema.json +34 -12
@@ -9,7 +9,7 @@ const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
9
9
  ? path.resolve(process.env.UBP_RUNTIME_ROOT)
10
10
  : path.resolve(__dirname, '..', '..');
11
11
  const SCHEMA_FILE = path.join(REPO_ROOT, 'spec', 'schemas', 'state-db.sql');
12
- const EXPECTED_VERSION = '22.0';
12
+ const EXPECTED_VERSION = '23.0';
13
13
  const KIMI_SCHEMA_VERSION = '9.1';
14
14
  const CONTEXT_SCHEMA_VERSION = '10.0';
15
15
  const BASELINE_SCHEMA_VERSION = '11.0';
@@ -24,6 +24,7 @@ const DECISION_COMPLETION_SCHEMA_VERSION = '19.0';
24
24
  const ARTIFACT_REGISTRY_SCHEMA_VERSION = '20.0';
25
25
  const KERNEL_SCHEMA_VERSION = '21.0';
26
26
  const CHECKPOINT_NATIVE_SCHEMA_VERSION = '22.0';
27
+ const SEMANTIC_KERNEL_SCHEMA_VERSION = '23.0';
27
28
 
28
29
  const MIGRATED_GAPS = Object.freeze([{
29
30
  id: 'legacy-rebaseline-required',
@@ -306,9 +307,154 @@ function applyChangeColumns(db) {
306
307
  db, 'changes', columns, 'alignment_thread_id',
307
308
  'TEXT REFERENCES decision_threads(id) ON DELETE SET NULL',
308
309
  ) || changed;
310
+ changed = addColumnIfMissing(
311
+ db, 'changes', columns, 'supersedes_id',
312
+ 'TEXT REFERENCES changes(id) ON DELETE SET NULL',
313
+ ) || changed;
309
314
  return changed;
310
315
  }
311
316
 
317
+ function upgradeSemanticKernelConstraints(db) {
318
+ const tables = new Set(tableNames(db));
319
+ if (!tables.has('changes') || !tables.has('tasks')) return false;
320
+ const taskColumns = columnNames(db, 'tasks');
321
+ const preservesLegacyComplexityHint = taskColumns.has('complexity_hint');
322
+ const changesSql = db.prepare(
323
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'changes'",
324
+ ).get()?.sql || '';
325
+ const tasksSql = db.prepare(
326
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'tasks'",
327
+ ).get()?.sql || '';
328
+ const current = /length\s*\(\s*trim\s*\(\s*kind\s*\)\s*\)\s+BETWEEN\s+1\s+AND\s+80/i
329
+ .test(changesSql)
330
+ && /length\s*\(\s*trim\s*\(\s*type\s*\)\s*\)\s+BETWEEN\s+1\s+AND\s+80/i
331
+ .test(tasksSql)
332
+ && /length\s*\(\s*trim\s*\(\s*priority\s*\)\s*\)\s+BETWEEN\s+1\s+AND\s+80/i
333
+ .test(tasksSql)
334
+ && /length\s*\(\s*trim\s*\(\s*slice_kind\s*\)\s*\)\s+BETWEEN\s+1\s+AND\s+80/i
335
+ .test(tasksSql)
336
+ && columnNames(db, 'changes').has('supersedes_id');
337
+ if (current) return false;
338
+
339
+ const foreignKeys = db.pragma('foreign_keys', { simple: true });
340
+ db.pragma('foreign_keys = OFF');
341
+ try {
342
+ db.transaction(() => {
343
+ db.exec(`
344
+ DROP TABLE IF EXISTS changes_semantic_kernel_upgrade;
345
+ CREATE TABLE changes_semantic_kernel_upgrade (
346
+ id TEXT PRIMARY KEY,
347
+ title TEXT NOT NULL,
348
+ kind TEXT NOT NULL CHECK (length(trim(kind)) BETWEEN 1 AND 80),
349
+ status TEXT NOT NULL DEFAULT 'active'
350
+ CHECK (status IN ('active', 'blocked', 'ready', 'archived', 'cancelled')),
351
+ intent TEXT NOT NULL,
352
+ docs_impact_json TEXT NOT NULL DEFAULT '{"status":"unknown","files":[],"rationale":null}',
353
+ provider_refs_json TEXT NOT NULL DEFAULT '{}',
354
+ baseline_bypass_json TEXT,
355
+ contract_json TEXT NOT NULL DEFAULT '{}',
356
+ classification_json TEXT NOT NULL DEFAULT '{}',
357
+ research_disposition_json TEXT NOT NULL DEFAULT '{}',
358
+ alignment_thread_id TEXT REFERENCES decision_threads(id) ON DELETE SET NULL,
359
+ base_commit TEXT,
360
+ supersedes_id TEXT REFERENCES changes_semantic_kernel_upgrade(id) ON DELETE SET NULL,
361
+ artifact_root TEXT NOT NULL,
362
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
363
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
364
+ closed_at TEXT
365
+ );
366
+ INSERT INTO changes_semantic_kernel_upgrade (
367
+ id, title, kind, status, intent, docs_impact_json, provider_refs_json,
368
+ baseline_bypass_json, contract_json, classification_json,
369
+ research_disposition_json, alignment_thread_id, base_commit,
370
+ supersedes_id, artifact_root, created_at, updated_at, closed_at
371
+ )
372
+ SELECT
373
+ id, title, kind, status, intent, docs_impact_json, provider_refs_json,
374
+ baseline_bypass_json, contract_json, classification_json,
375
+ research_disposition_json, alignment_thread_id, base_commit,
376
+ supersedes_id, artifact_root, created_at, updated_at, closed_at
377
+ FROM changes;
378
+
379
+ DROP TABLE IF EXISTS tasks_semantic_kernel_upgrade;
380
+ CREATE TABLE tasks_semantic_kernel_upgrade (
381
+ id TEXT PRIMARY KEY,
382
+ title TEXT NOT NULL,
383
+ type TEXT NOT NULL CHECK (length(trim(type)) BETWEEN 1 AND 80),
384
+ priority TEXT NOT NULL CHECK (length(trim(priority)) BETWEEN 1 AND 80),
385
+ complexity INTEGER CHECK (complexity BETWEEN 1 AND 10),
386
+ estimated_days REAL CHECK (estimated_days IS NULL OR estimated_days > 0),
387
+ status TEXT NOT NULL DEFAULT 'pending'
388
+ CHECK (status IN ('pending', 'in_progress', 'completed', 'blocked', 'expanded')),
389
+ deps TEXT,
390
+ files_modified TEXT,
391
+ session_id TEXT,
392
+ stale INTEGER NOT NULL DEFAULT 0 CHECK (stale IN (0, 1)),
393
+ ${preservesLegacyComplexityHint ? 'complexity_hint TEXT,' : ''}
394
+ tag TEXT,
395
+ trace_to TEXT,
396
+ outcome TEXT,
397
+ slice_kind TEXT CHECK (
398
+ slice_kind IS NULL OR length(trim(slice_kind)) BETWEEN 1 AND 80
399
+ ),
400
+ public_seam TEXT,
401
+ verification_command TEXT,
402
+ acceptance_json TEXT NOT NULL DEFAULT '[]',
403
+ context_refs_json TEXT NOT NULL DEFAULT '[]',
404
+ docs_impact_json TEXT NOT NULL DEFAULT '{"status":"unknown","files":[],"rationale":null}',
405
+ ownership_json TEXT NOT NULL DEFAULT '{}',
406
+ context_file TEXT,
407
+ completion_commit TEXT,
408
+ change_id TEXT REFERENCES changes_semantic_kernel_upgrade(id) ON DELETE SET NULL,
409
+ parent_id TEXT REFERENCES tasks_semantic_kernel_upgrade(id) ON DELETE SET NULL,
410
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
411
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
412
+ );
413
+ INSERT INTO tasks_semantic_kernel_upgrade (
414
+ id, title, type, priority, complexity, estimated_days, status, deps,
415
+ files_modified, session_id, stale,
416
+ ${preservesLegacyComplexityHint ? 'complexity_hint,' : ''}
417
+ tag, trace_to, outcome, slice_kind,
418
+ public_seam, verification_command, acceptance_json, context_refs_json,
419
+ docs_impact_json, ownership_json, context_file, completion_commit,
420
+ change_id, parent_id, created_at, updated_at
421
+ )
422
+ SELECT
423
+ id, title, type, priority, complexity, estimated_days, status, deps,
424
+ files_modified, session_id, stale,
425
+ ${preservesLegacyComplexityHint ? 'complexity_hint,' : ''}
426
+ tag, trace_to, outcome, slice_kind,
427
+ public_seam, verification_command, acceptance_json, context_refs_json,
428
+ docs_impact_json, ownership_json, context_file, completion_commit,
429
+ change_id, parent_id, created_at, updated_at
430
+ FROM tasks;
431
+
432
+ DROP TABLE tasks;
433
+ DROP TABLE changes;
434
+ ALTER TABLE changes_semantic_kernel_upgrade RENAME TO changes;
435
+ ALTER TABLE tasks_semantic_kernel_upgrade RENAME TO tasks;
436
+ CREATE INDEX changes_status ON changes(status, created_at);
437
+ CREATE INDEX changes_kind ON changes(kind, created_at);
438
+ CREATE INDEX tasks_status ON tasks(status);
439
+ CREATE INDEX tasks_tag ON tasks(tag);
440
+ CREATE INDEX tasks_session ON tasks(session_id) WHERE session_id IS NOT NULL;
441
+ CREATE INDEX tasks_stale ON tasks(stale) WHERE stale = 1;
442
+ CREATE INDEX tasks_parent ON tasks(parent_id) WHERE parent_id IS NOT NULL;
443
+ CREATE INDEX tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL;
444
+ `);
445
+ const violations = db.pragma('foreign_key_check');
446
+ if (violations.length > 0) {
447
+ throw new Error(
448
+ `semantic kernel migration produced ${violations.length} foreign key violation(s)`,
449
+ );
450
+ }
451
+ })();
452
+ } finally {
453
+ db.pragma(`foreign_keys = ${foreignKeys ? 'ON' : 'OFF'}`);
454
+ }
455
+ return true;
456
+ }
457
+
312
458
  function applySemanticAuthorityColumns(db) {
313
459
  let changed = false;
314
460
  if (tableNames(db).includes('workflow_steps')) {
@@ -416,7 +562,7 @@ function applyCompatibleColumns(db) {
416
562
  }
417
563
  const taskAdditions = [
418
564
  ['outcome', 'TEXT'],
419
- ['slice_kind', "TEXT CHECK (slice_kind IS NULL OR slice_kind IN ('tracer_bullet', 'expand_contract', 'integration_checkpoint'))"],
565
+ ['slice_kind', 'TEXT CHECK (slice_kind IS NULL OR length(trim(slice_kind)) BETWEEN 1 AND 80)'],
420
566
  ['public_seam', 'TEXT'],
421
567
  ['verification_command', 'TEXT'],
422
568
  ['acceptance_json', "TEXT NOT NULL DEFAULT '[]'"],
@@ -486,7 +632,7 @@ function migrateAdaptiveWorkflowRuns(db, fromVersion = latestSchemaVersion(db))
486
632
  if ([
487
633
  ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
488
634
  ARTIFACT_REGISTRY_SCHEMA_VERSION, KERNEL_SCHEMA_VERSION,
489
- CHECKPOINT_NATIVE_SCHEMA_VERSION,
635
+ CHECKPOINT_NATIVE_SCHEMA_VERSION, SEMANTIC_KERNEL_SCHEMA_VERSION,
490
636
  ].includes(fromVersion) || existing) {
491
637
  return false;
492
638
  }
@@ -1007,6 +1153,11 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1007
1153
  }
1008
1154
  const gitStateChangedFrom = upgradeBaselineWorktreeConstraint(db, fromVersion);
1009
1155
  const decisionCompletionChanged = upgradeDecisionThreadLifecycle(db);
1156
+ db.transaction(() => {
1157
+ applyCompatibleColumns(db);
1158
+ applyChangeColumns(db);
1159
+ })();
1160
+ const semanticKernelChanged = upgradeSemanticKernelConstraints(db);
1010
1161
  db.transaction(() => {
1011
1162
  applyCompatibleColumns(db);
1012
1163
  const contextChanged = applyContextSpineUpgrade(db);
@@ -1020,6 +1171,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1020
1171
  DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
1021
1172
  DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
1022
1173
  KERNEL_SCHEMA_VERSION, CHECKPOINT_NATIVE_SCHEMA_VERSION,
1174
+ SEMANTIC_KERNEL_SCHEMA_VERSION,
1023
1175
  ].includes(fromVersion)
1024
1176
  && !contextMigration,
1025
1177
  );
@@ -1049,6 +1201,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1049
1201
  GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
1050
1202
  DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
1051
1203
  KERNEL_SCHEMA_VERSION, CHECKPOINT_NATIVE_SCHEMA_VERSION,
1204
+ SEMANTIC_KERNEL_SCHEMA_VERSION,
1052
1205
  ].includes(fromVersion)
1053
1206
  && !baselineMigration) {
1054
1207
  db.prepare(
@@ -1123,6 +1276,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1123
1276
  DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
1124
1277
  DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
1125
1278
  KERNEL_SCHEMA_VERSION, CHECKPOINT_NATIVE_SCHEMA_VERSION,
1279
+ SEMANTIC_KERNEL_SCHEMA_VERSION,
1126
1280
  ].includes(fromVersion) && !workflowMigration) {
1127
1281
  db.prepare(
1128
1282
  `INSERT INTO migration_history
@@ -1141,7 +1295,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1141
1295
  AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION,
1142
1296
  GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
1143
1297
  ARTIFACT_REGISTRY_SCHEMA_VERSION, KERNEL_SCHEMA_VERSION,
1144
- CHECKPOINT_NATIVE_SCHEMA_VERSION,
1298
+ CHECKPOINT_NATIVE_SCHEMA_VERSION, SEMANTIC_KERNEL_SCHEMA_VERSION,
1145
1299
  ].includes(fromVersion) && !authorityMigration) {
1146
1300
  db.prepare(
1147
1301
  `INSERT INTO migration_history
@@ -1160,7 +1314,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1160
1314
  SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION,
1161
1315
  ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
1162
1316
  ARTIFACT_REGISTRY_SCHEMA_VERSION, KERNEL_SCHEMA_VERSION,
1163
- CHECKPOINT_NATIVE_SCHEMA_VERSION,
1317
+ CHECKPOINT_NATIVE_SCHEMA_VERSION, SEMANTIC_KERNEL_SCHEMA_VERSION,
1164
1318
  ].includes(fromVersion) && !semanticMigration) {
1165
1319
  db.prepare(
1166
1320
  `INSERT INTO migration_history
@@ -1179,6 +1333,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1179
1333
  DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
1180
1334
  DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
1181
1335
  KERNEL_SCHEMA_VERSION, CHECKPOINT_NATIVE_SCHEMA_VERSION,
1336
+ SEMANTIC_KERNEL_SCHEMA_VERSION,
1182
1337
  ].includes(fromVersion)
1183
1338
  && !dialogueMigration) {
1184
1339
  db.prepare(
@@ -1197,7 +1352,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1197
1352
  if (fromVersion && ![
1198
1353
  GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
1199
1354
  ARTIFACT_REGISTRY_SCHEMA_VERSION, KERNEL_SCHEMA_VERSION,
1200
- CHECKPOINT_NATIVE_SCHEMA_VERSION,
1355
+ CHECKPOINT_NATIVE_SCHEMA_VERSION, SEMANTIC_KERNEL_SCHEMA_VERSION,
1201
1356
  ].includes(fromVersion)
1202
1357
  && !gitStateMigration) {
1203
1358
  db.prepare(
@@ -1217,7 +1372,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1217
1372
  if (fromVersion && ![
1218
1373
  ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
1219
1374
  ARTIFACT_REGISTRY_SCHEMA_VERSION, KERNEL_SCHEMA_VERSION,
1220
- CHECKPOINT_NATIVE_SCHEMA_VERSION,
1375
+ CHECKPOINT_NATIVE_SCHEMA_VERSION, SEMANTIC_KERNEL_SCHEMA_VERSION,
1221
1376
  ].includes(fromVersion) && !adaptiveMigration) {
1222
1377
  db.prepare(
1223
1378
  `INSERT INTO migration_history
@@ -1237,6 +1392,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1237
1392
  if (fromVersion && ![
1238
1393
  DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
1239
1394
  KERNEL_SCHEMA_VERSION, CHECKPOINT_NATIVE_SCHEMA_VERSION,
1395
+ SEMANTIC_KERNEL_SCHEMA_VERSION,
1240
1396
  ].includes(fromVersion) && !completionMigration) {
1241
1397
  db.prepare(
1242
1398
  `INSERT INTO migration_history
@@ -1256,6 +1412,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1256
1412
  if (fromVersion && fromVersion !== ARTIFACT_REGISTRY_SCHEMA_VERSION
1257
1413
  && fromVersion !== KERNEL_SCHEMA_VERSION
1258
1414
  && fromVersion !== CHECKPOINT_NATIVE_SCHEMA_VERSION
1415
+ && fromVersion !== SEMANTIC_KERNEL_SCHEMA_VERSION
1259
1416
  && !artifactRegistryMigration) {
1260
1417
  db.prepare(
1261
1418
  `INSERT INTO migration_history
@@ -1275,6 +1432,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1275
1432
  if (fromVersion
1276
1433
  && fromVersion !== KERNEL_SCHEMA_VERSION
1277
1434
  && fromVersion !== CHECKPOINT_NATIVE_SCHEMA_VERSION
1435
+ && fromVersion !== SEMANTIC_KERNEL_SCHEMA_VERSION
1278
1436
  && !kernelMigration) {
1279
1437
  db.prepare(
1280
1438
  `INSERT INTO migration_history
@@ -1291,6 +1449,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1291
1449
  ).get();
1292
1450
  if (fromVersion
1293
1451
  && fromVersion !== CHECKPOINT_NATIVE_SCHEMA_VERSION
1452
+ && fromVersion !== SEMANTIC_KERNEL_SCHEMA_VERSION
1294
1453
  && !checkpointNativeMigration) {
1295
1454
  db.prepare(
1296
1455
  `INSERT INTO migration_history
@@ -1302,6 +1461,24 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1302
1461
  'Bind Baseline research to Stage Checkpoints and add byte digests for Context Envelopes and Worker Packets',
1303
1462
  );
1304
1463
  }
1464
+ const semanticKernelMigration = db.prepare(
1465
+ "SELECT 1 FROM migration_history WHERE to_version = '23.0' AND status = 'success' LIMIT 1",
1466
+ ).get();
1467
+ if (fromVersion
1468
+ && fromVersion !== SEMANTIC_KERNEL_SCHEMA_VERSION
1469
+ && !semanticKernelMigration) {
1470
+ db.prepare(
1471
+ `INSERT INTO migration_history
1472
+ (from_version, to_version, direction, status, notes)
1473
+ VALUES (?, ?, 'forward', 'success', ?)`,
1474
+ ).run(
1475
+ CHECKPOINT_NATIVE_SCHEMA_VERSION,
1476
+ SEMANTIC_KERNEL_SCHEMA_VERSION,
1477
+ semanticKernelChanged
1478
+ ? 'Move Task and Change business vocabulary out of SQLite enums and add immutable Change successor linkage'
1479
+ : 'Record semantic-kernel vocabulary and Change successor authority',
1480
+ );
1481
+ }
1305
1482
  db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
1306
1483
  db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
1307
1484
  ensureActiveSessionLeaseIndex(db);
@@ -1309,7 +1486,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
1309
1486
  'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
1310
1487
  ).run(
1311
1488
  EXPECTED_VERSION,
1312
- 'Checkpoint-native baseline authority plus byte-bound Context Envelopes and Worker Packets',
1489
+ 'Persistence and safety kernel with open semantic vocabulary and immutable Change successors',
1313
1490
  );
1314
1491
  })();
1315
1492
  }
@@ -1343,6 +1520,7 @@ function initStateDb(dbPath) {
1343
1520
  DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
1344
1521
  DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
1345
1522
  KERNEL_SCHEMA_VERSION, CHECKPOINT_NATIVE_SCHEMA_VERSION,
1523
+ SEMANTIC_KERNEL_SCHEMA_VERSION,
1346
1524
  ].includes(fromVersion);
1347
1525
  backupPath = migrationBackup(db, dbPath, fromVersion, existing);
1348
1526
  // This must precede every ALTER, table rebuild, or index creation. Runtime
@@ -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
- 'priority', 'complexity', 'estimated_days', 'deps', 'files_modified',
39
- 'session_id', 'stale', 'tag', 'trace_to',
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 TASK_CONTRACT_PATCH_FIELDS = new Set([
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 !== false,
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 (!['tracer_bullet', 'expand_contract', 'integration_checkpoint'].includes(task?.slice_kind)) {
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((field) => TASK_CONTRACT_PATCH_FIELDS.has(field));
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 t = readTask(db, id);
603
- if (!t) throw new StateOpsError('TASK_NOT_FOUND', `no task ${id}`);
604
- if (t.session_id && !force) {
605
- throw new StateOpsError('SESSION_ACTIVE', `task ${id} has session ${t.session_id}; pass force=true to override`);
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
- return { ok: true };
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 || !event.type) {
616
- throw new StateOpsError('VALIDATION_ERROR', 'event.type is required');
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
- if (event.runtime != null && !isSupportedRuntime(event.runtime)) {
619
- throw new StateOpsError('VALIDATION_ERROR', `unsupported runtime: ${event.runtime}`);
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
- event.type,
626
- event.task_id ?? null,
627
- event.change_id ?? null,
628
- event.session_id ?? null,
629
- event.runtime ?? null,
630
- event.payload === undefined ? null : JSON.stringify(event.payload),
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 };