ultra-builder-pro-cli 0.14.0 → 0.15.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 (154) hide show
  1. package/.ultra-template/docs/research/README.md +11 -7
  2. package/.ultra-template/specs/architecture.md +36 -36
  3. package/.ultra-template/specs/discovery.md +46 -33
  4. package/.ultra-template/specs/product.md +38 -29
  5. package/.ultra-template/specs/research-distillate.md +48 -0
  6. package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
  7. package/CHANGELOG.md +44 -1
  8. package/README.md +34 -19
  9. package/adapters/_shared/codex-assets.cjs +6 -2
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/claude.js +3 -2
  12. package/adapters/codex.js +1 -0
  13. package/adapters/kimi.js +3 -2
  14. package/adapters/opencode.js +3 -2
  15. package/agents/review-coordinator.md +9 -6
  16. package/docs/AGENT-CONTEXT.md +15 -8
  17. package/docs/ARCHITECTURE.md +33 -12
  18. package/docs/PLAN.zh-CN.md +3 -2
  19. package/docs/ROADMAP.md +8 -4
  20. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  21. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  22. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  23. package/hooks/README.md +6 -1
  24. package/hooks/health_check.py +12 -3
  25. package/hooks/subagent_tracker.py +52 -77
  26. package/mcp-server/hook-event.cjs +34 -0
  27. package/mcp-server/lib/archive-journal.cjs +17 -5
  28. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  29. package/mcp-server/lib/change-workflow.cjs +550 -85
  30. package/mcp-server/lib/context-spine.cjs +242 -24
  31. package/mcp-server/lib/doctor.cjs +33 -3
  32. package/mcp-server/lib/init-project.cjs +210 -16
  33. package/mcp-server/lib/plan-store.cjs +4 -1
  34. package/mcp-server/lib/pricing.cjs +7 -10
  35. package/mcp-server/lib/project-breadcrumb.cjs +6 -0
  36. package/mcp-server/lib/projector.cjs +57 -3
  37. package/mcp-server/lib/spec-learning.cjs +79 -2
  38. package/mcp-server/lib/state-db.cjs +145 -8
  39. package/mcp-server/lib/state-ops.cjs +160 -9
  40. package/mcp-server/lib/workflow-state.cjs +2292 -0
  41. package/mcp-server/server.cjs +161 -15
  42. package/orchestrator/daemon.cjs +1 -2
  43. package/orchestrator/dispatch-rules.cjs +5 -31
  44. package/orchestrator/planner/plan-builder.cjs +21 -12
  45. package/package.json +13 -3
  46. package/skills/ultra-change/SKILL.md +53 -68
  47. package/skills/ultra-change/references/change-contract.md +36 -0
  48. package/skills/ultra-deliver/SKILL.md +69 -79
  49. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  50. package/skills/ultra-dev/SKILL.md +54 -59
  51. package/skills/ultra-doctor/SKILL.md +40 -71
  52. package/skills/ultra-init/SKILL.md +96 -103
  53. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  54. package/skills/ultra-plan/SKILL.md +79 -85
  55. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  56. package/skills/ultra-research/SKILL.md +87 -60
  57. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  58. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  59. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  60. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  61. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  62. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  63. package/skills/ultra-research/references/10-user-personas.md +29 -0
  64. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  65. package/skills/ultra-research/references/20-user-stories.md +30 -0
  66. package/skills/ultra-research/references/21-features-scope.md +29 -0
  67. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  68. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  69. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  70. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  71. package/skills/ultra-research/references/40-deployment.md +29 -0
  72. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  73. package/skills/ultra-research/references/99-synthesis.md +35 -0
  74. package/skills/ultra-review/SKILL.md +57 -73
  75. package/skills/ultra-review/references/review-modes.md +27 -0
  76. package/skills/ultra-review/references/unified-schema.md +25 -0
  77. package/skills/ultra-review/scripts/review_wait.py +40 -3
  78. package/skills/ultra-status/SKILL.md +41 -45
  79. package/skills/ultra-test/SKILL.md +48 -55
  80. package/skills/ultra-verify/SKILL.md +1 -1
  81. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  82. package/spec/cli-protocol.md +7 -2
  83. package/spec/mcp-tools.yaml +384 -63
  84. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  85. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  86. package/spec/schemas/mcp-tools.schema.json +1 -1
  87. package/spec/schemas/state-db.sql +82 -1
  88. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  89. package/spec/schemas/test-report.v1.schema.json +194 -0
  90. package/ultra-tools/cli.cjs +1 -1
  91. package/ultra-tools/commands/migrate.cjs +3 -3
  92. package/ultra-tools/commands/status.cjs +213 -12
  93. package/.ultra-template/delivery-report.json +0 -9
  94. package/.ultra-template/test-report.json +0 -8
  95. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  96. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  97. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  98. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  99. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  100. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  101. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -149
  102. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  103. package/adapters/_shared/tests/validate.test.cjs +0 -30
  104. package/adapters/tests/claude.test.cjs +0 -138
  105. package/adapters/tests/codex-hook.test.cjs +0 -142
  106. package/adapters/tests/codex.test.cjs +0 -430
  107. package/adapters/tests/kimi-hook.test.cjs +0 -149
  108. package/adapters/tests/kimi.test.cjs +0 -259
  109. package/adapters/tests/opencode.test.cjs +0 -357
  110. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  111. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  112. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  113. package/mcp-server/lib/context-spine.test.cjs +0 -93
  114. package/mcp-server/lib/doctor.test.cjs +0 -148
  115. package/mcp-server/lib/plan-store.test.cjs +0 -132
  116. package/mcp-server/lib/prd-parser.test.cjs +0 -82
  117. package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
  118. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  119. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  120. package/mcp-server/lib/state-db.test.cjs +0 -314
  121. package/mcp-server/lib/task-expander.test.cjs +0 -169
  122. package/mcp-server/lib/topo.test.cjs +0 -158
  123. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  124. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  125. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  126. package/mcp-server/tests/concurrency.test.cjs +0 -112
  127. package/mcp-server/tests/db-ops.test.cjs +0 -121
  128. package/mcp-server/tests/init-project.test.cjs +0 -393
  129. package/mcp-server/tests/migrate.test.cjs +0 -232
  130. package/mcp-server/tests/projector.test.cjs +0 -162
  131. package/mcp-server/tests/server-contract.test.cjs +0 -1467
  132. package/mcp-server/tests/staleness.test.cjs +0 -190
  133. package/mcp-server/tests/state-db.test.cjs +0 -106
  134. package/mcp-server/tests/state-ops.test.cjs +0 -449
  135. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  136. package/mcp-server/tests/telemetry.test.cjs +0 -219
  137. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  138. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  139. package/orchestrator/tests/daemon.test.cjs +0 -311
  140. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  141. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  142. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  143. package/orchestrator/tests/recovery.test.cjs +0 -190
  144. package/orchestrator/tests/session-runner.test.cjs +0 -314
  145. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  146. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  147. package/skills/ultra-research/references/architecture.md +0 -38
  148. package/skills/ultra-research/references/problem-product.md +0 -39
  149. package/skills/ultra-research/references/quality-delivery.md +0 -34
  150. package/skills/ultra-research/references/synthesis.md +0 -38
  151. package/skills/ultra-research/references/users-scope.md +0 -40
  152. package/ultra-tools/legacy-memory.test.cjs +0 -80
  153. package/ultra-tools/status-command.test.cjs +0 -181
  154. package/ultra-tools/system-command.test.cjs +0 -309
@@ -9,10 +9,13 @@ 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 = '12.0';
12
+ const EXPECTED_VERSION = '15.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';
16
+ const ADOPTION_SCHEMA_VERSION = '12.0';
17
+ const WORKFLOW_SCHEMA_VERSION = '13.0';
18
+ const AUTHORITY_SCHEMA_VERSION = '14.0';
16
19
 
17
20
  const MIGRATED_GAPS = Object.freeze([{
18
21
  id: 'legacy-rebaseline-required',
@@ -42,6 +45,8 @@ const REQUIRED_TABLES = Object.freeze([
42
45
  'incidents',
43
46
  'projection_jobs',
44
47
  'event_consumers',
48
+ 'workflow_runs',
49
+ 'workflow_steps',
45
50
  ]);
46
51
 
47
52
  function applyBaselineUpgrade(db, { legacyState = false } = {}) {
@@ -63,6 +68,7 @@ function applyBaselineUpgrade(db, { legacyState = false } = {}) {
63
68
  worktree_digest TEXT,
64
69
  worktree_files_json TEXT NOT NULL DEFAULT '[]',
65
70
  worktree_accepted INTEGER NOT NULL DEFAULT 0 CHECK (worktree_accepted IN (0, 1)),
71
+ known_red_accepted INTEGER NOT NULL DEFAULT 0 CHECK (known_red_accepted IN (0, 1)),
66
72
  spec_refs_json TEXT NOT NULL DEFAULT '[]',
67
73
  evidence_json TEXT NOT NULL DEFAULT '[]',
68
74
  verification_json TEXT NOT NULL DEFAULT '[]',
@@ -70,6 +76,7 @@ function applyBaselineUpgrade(db, { legacyState = false } = {}) {
70
76
  gaps_json TEXT NOT NULL DEFAULT '[]',
71
77
  classification_json TEXT NOT NULL DEFAULT '{}',
72
78
  provider_refs_json TEXT NOT NULL DEFAULT '{}',
79
+ research_run_id TEXT,
73
80
  approved_by TEXT,
74
81
  approval_note TEXT,
75
82
  started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
@@ -114,17 +121,51 @@ function applyBaselineColumns(db) {
114
121
  db, 'baselines', columns, 'worktree_accepted',
115
122
  'INTEGER NOT NULL DEFAULT 0 CHECK (worktree_accepted IN (0, 1))',
116
123
  ) || changed;
124
+ changed = addColumnIfMissing(
125
+ db, 'baselines', columns, 'known_red_accepted',
126
+ 'INTEGER NOT NULL DEFAULT 0 CHECK (known_red_accepted IN (0, 1))',
127
+ ) || changed;
117
128
  changed = addColumnIfMissing(db, 'baselines', columns, 'gaps_json', "TEXT NOT NULL DEFAULT '[]'") || changed;
118
129
  changed = addColumnIfMissing(
119
130
  db, 'baselines', columns, 'classification_json', "TEXT NOT NULL DEFAULT '{}'",
120
131
  ) || changed;
132
+ changed = addColumnIfMissing(db, 'baselines', columns, 'research_run_id', 'TEXT') || changed;
121
133
  return changed;
122
134
  }
123
135
 
124
136
  function applyChangeColumns(db) {
125
137
  if (!tableNames(db).includes('changes')) return false;
126
138
  const columns = columnNames(db, 'changes');
127
- return addColumnIfMissing(db, 'changes', columns, 'baseline_bypass_json', 'TEXT');
139
+ let changed = false;
140
+ changed = addColumnIfMissing(db, 'changes', columns, 'baseline_bypass_json', 'TEXT') || changed;
141
+ changed = addColumnIfMissing(db, 'changes', columns, 'contract_json', "TEXT NOT NULL DEFAULT '{}'") || changed;
142
+ changed = addColumnIfMissing(db, 'changes', columns, 'classification_json', "TEXT NOT NULL DEFAULT '{}'") || changed;
143
+ changed = addColumnIfMissing(
144
+ db, 'changes', columns, 'research_disposition_json', "TEXT NOT NULL DEFAULT '{}'",
145
+ ) || changed;
146
+ return changed;
147
+ }
148
+
149
+ function applySemanticAuthorityColumns(db) {
150
+ let changed = false;
151
+ if (tableNames(db).includes('workflow_steps')) {
152
+ const columns = columnNames(db, 'workflow_steps');
153
+ changed = addColumnIfMissing(
154
+ db, 'workflow_steps', columns, 'semantic_records_json', "TEXT NOT NULL DEFAULT '[]'",
155
+ ) || changed;
156
+ }
157
+ if (tableNames(db).includes('spec_learning_candidates')) {
158
+ const columns = columnNames(db, 'spec_learning_candidates');
159
+ for (const [name, definition] of [
160
+ ['applied_ref', 'TEXT'],
161
+ ['before_digest', 'TEXT'],
162
+ ['after_digest', 'TEXT'],
163
+ ['apply_evidence_json', "TEXT NOT NULL DEFAULT '[]'"],
164
+ ]) {
165
+ changed = addColumnIfMissing(db, 'spec_learning_candidates', columns, name, definition) || changed;
166
+ }
167
+ }
168
+ return changed;
128
169
  }
129
170
 
130
171
  function quoteSqlString(value) {
@@ -151,7 +192,14 @@ function readSchemaSql() {
151
192
  }
152
193
 
153
194
  function applyPragmas(db) {
154
- db.pragma('journal_mode = WAL');
195
+ const journalMode = db.pragma('journal_mode = WAL', { simple: true });
196
+ if (String(journalMode).toLowerCase() !== 'wal') {
197
+ const error = new Error(
198
+ `state.db requires SQLite WAL mode; storage returned ${String(journalMode || 'unknown')}`,
199
+ );
200
+ error.code = 'STATE_DB_WAL_UNAVAILABLE';
201
+ throw error;
202
+ }
155
203
  db.pragma('synchronous = NORMAL');
156
204
  db.pragma('busy_timeout = 5000');
157
205
  db.pragma('foreign_keys = ON');
@@ -202,6 +250,19 @@ function applyCompatibleColumns(db) {
202
250
  if (!taskColumns.has('change_id')) {
203
251
  db.exec('ALTER TABLE tasks ADD COLUMN change_id TEXT REFERENCES changes(id) ON DELETE SET NULL');
204
252
  }
253
+ const taskAdditions = [
254
+ ['outcome', 'TEXT'],
255
+ ['slice_kind', "TEXT CHECK (slice_kind IS NULL OR slice_kind IN ('tracer_bullet', 'expand_contract', 'integration_checkpoint'))"],
256
+ ['public_seam', 'TEXT'],
257
+ ['verification_command', 'TEXT'],
258
+ ['acceptance_json', "TEXT NOT NULL DEFAULT '[]'"],
259
+ ['context_refs_json', "TEXT NOT NULL DEFAULT '[]'"],
260
+ ['docs_impact_json', "TEXT NOT NULL DEFAULT '{\"status\":\"unknown\",\"files\":[],\"rationale\":null}'"],
261
+ ['ownership_json', "TEXT NOT NULL DEFAULT '{}'"],
262
+ ];
263
+ for (const [name, definition] of taskAdditions) {
264
+ if (!taskColumns.has(name)) db.exec(`ALTER TABLE tasks ADD COLUMN ${name} ${definition}`);
265
+ }
205
266
  if (tables.has('events')) {
206
267
  const eventColumns = columnNames(db, 'events');
207
268
  if (!eventColumns.has('change_id')) db.exec('ALTER TABLE events ADD COLUMN change_id TEXT');
@@ -339,7 +400,10 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
339
400
  "SELECT 1 FROM migration_history WHERE to_version = '10.0' AND status = 'success' LIMIT 1",
340
401
  ).get();
341
402
  const needsContextMigration = Boolean(
342
- fromVersion && ![CONTEXT_SCHEMA_VERSION, BASELINE_SCHEMA_VERSION, EXPECTED_VERSION].includes(fromVersion)
403
+ fromVersion && ![
404
+ CONTEXT_SCHEMA_VERSION, BASELINE_SCHEMA_VERSION, ADOPTION_SCHEMA_VERSION,
405
+ WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, EXPECTED_VERSION,
406
+ ].includes(fromVersion)
343
407
  && !contextMigration,
344
408
  );
345
409
  if (contextChanged || needsContextMigration) {
@@ -356,10 +420,14 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
356
420
  applyBaselineUpgrade(db, { legacyState });
357
421
  applyBaselineColumns(db);
358
422
  applyChangeColumns(db);
423
+ applySemanticAuthorityColumns(db);
359
424
  const baselineMigration = db.prepare(
360
425
  "SELECT 1 FROM migration_history WHERE to_version = '11.0' AND status = 'success' LIMIT 1",
361
426
  ).get();
362
- if (fromVersion && ![BASELINE_SCHEMA_VERSION, EXPECTED_VERSION].includes(fromVersion)
427
+ if (fromVersion && ![
428
+ BASELINE_SCHEMA_VERSION, ADOPTION_SCHEMA_VERSION, WORKFLOW_SCHEMA_VERSION,
429
+ AUTHORITY_SCHEMA_VERSION, EXPECTED_VERSION,
430
+ ].includes(fromVersion)
363
431
  && !baselineMigration) {
364
432
  db.prepare(
365
433
  `INSERT INTO migration_history
@@ -372,6 +440,32 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
372
440
  );
373
441
  }
374
442
  if (legacyState) {
443
+ const legacyBaselines = db.prepare(
444
+ "SELECT id, mode, status, classification_json, gaps_json FROM baselines WHERE status != 'superseded'",
445
+ ).all();
446
+ for (const baseline of legacyBaselines) {
447
+ let classification = {};
448
+ let gaps = [];
449
+ try { classification = JSON.parse(baseline.classification_json || '{}'); } catch { classification = {}; }
450
+ try { gaps = JSON.parse(baseline.gaps_json || '[]'); } catch { gaps = []; }
451
+ if (!gaps.some((gap) => gap?.id === MIGRATED_GAPS[0].id)) gaps.push(MIGRATED_GAPS[0]);
452
+ classification.migration = {
453
+ previous_mode: baseline.mode,
454
+ previous_status: baseline.status,
455
+ from_schema: fromVersion || 'unknown',
456
+ requires_research_workflow: true,
457
+ };
458
+ db.prepare(
459
+ `UPDATE baselines SET mode = 'migrated', status = 'adopting',
460
+ classification_json = ?, gaps_json = ?, approved_by = NULL, approval_note = ?,
461
+ converged_at = NULL, research_run_id = NULL, updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
462
+ WHERE id = ?`,
463
+ ).run(
464
+ JSON.stringify(classification), JSON.stringify(gaps),
465
+ 'Legacy project preserved; evidence-backed brownfield re-adoption is required',
466
+ baseline.id,
467
+ );
468
+ }
375
469
  db.prepare(
376
470
  `UPDATE baselines SET status = 'adopting', approved_by = NULL, approval_note = ?,
377
471
  converged_at = NULL, worktree_state = 'unavailable', worktree_digest = NULL,
@@ -393,18 +487,60 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
393
487
  VALUES (?, ?, 'forward', 'success', ?)`,
394
488
  ).run(
395
489
  BASELINE_SCHEMA_VERSION,
396
- EXPECTED_VERSION,
490
+ ADOPTION_SCHEMA_VERSION,
397
491
  'Add repository snapshots, authoritative gap ledger, incident governance, and required re-adoption',
398
492
  );
399
493
  }
400
494
  }
495
+ const workflowMigration = db.prepare(
496
+ "SELECT 1 FROM migration_history WHERE to_version = '13.0' AND status = 'success' LIMIT 1",
497
+ ).get();
498
+ if (fromVersion && fromVersion !== EXPECTED_VERSION && !workflowMigration) {
499
+ db.prepare(
500
+ `INSERT INTO migration_history
501
+ (from_version, to_version, direction, status, notes)
502
+ VALUES (?, ?, 'forward', 'success', ?)`,
503
+ ).run(
504
+ fromVersion === ADOPTION_SCHEMA_VERSION ? fromVersion : ADOPTION_SCHEMA_VERSION,
505
+ WORKFLOW_SCHEMA_VERSION,
506
+ 'Add durable cross-stage workflow runs, step evidence, output digests, blockers, and recovery',
507
+ );
508
+ }
509
+ const authorityMigration = db.prepare(
510
+ "SELECT 1 FROM migration_history WHERE to_version = '14.0' AND status = 'success' LIMIT 1",
511
+ ).get();
512
+ if (fromVersion && fromVersion !== EXPECTED_VERSION && !authorityMigration) {
513
+ db.prepare(
514
+ `INSERT INTO migration_history
515
+ (from_version, to_version, direction, status, notes)
516
+ VALUES (?, ?, 'forward', 'success', ?)`,
517
+ ).run(
518
+ fromVersion === WORKFLOW_SCHEMA_VERSION ? fromVersion : WORKFLOW_SCHEMA_VERSION,
519
+ AUTHORITY_SCHEMA_VERSION,
520
+ 'Persist known-red acceptance and continuously revalidate ready baseline authority',
521
+ );
522
+ }
523
+ const semanticMigration = db.prepare(
524
+ "SELECT 1 FROM migration_history WHERE to_version = '15.0' AND status = 'success' LIMIT 1",
525
+ ).get();
526
+ if (fromVersion && fromVersion !== EXPECTED_VERSION && !semanticMigration) {
527
+ db.prepare(
528
+ `INSERT INTO migration_history
529
+ (from_version, to_version, direction, status, notes)
530
+ VALUES (?, ?, 'forward', 'success', ?)`,
531
+ ).run(
532
+ fromVersion === AUTHORITY_SCHEMA_VERSION ? fromVersion : AUTHORITY_SCHEMA_VERSION,
533
+ EXPECTED_VERSION,
534
+ 'Add typed research semantics, complete change contracts, verified learning application, and reconciliation provenance',
535
+ );
536
+ }
401
537
  db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
402
538
  db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
403
539
  db.prepare(
404
540
  'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
405
541
  ).run(
406
542
  EXPECTED_VERSION,
407
- 'Evidence-backed repository snapshots, gap ledger, safe re-adoption, and incident break-glass governance',
543
+ 'Typed research semantics, complete change contracts, verified learning application, and reconciliation provenance',
408
544
  );
409
545
  })();
410
546
  }
@@ -431,7 +567,8 @@ function initStateDb(dbPath) {
431
567
  db = openStateDb(dbPath);
432
568
  const existing = new Set(tableNames(db));
433
569
  const fromVersion = latestSchemaVersion(db);
434
- const legacyState = existing.size > 0 && fromVersion !== EXPECTED_VERSION;
570
+ const legacyState = existing.size > 0
571
+ && ![WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, EXPECTED_VERSION].includes(fromVersion);
435
572
  backupPath = migrationBackup(db, dbPath, fromVersion, existing);
436
573
  applyCompatibleColumns(db);
437
574
  const missing = REQUIRED_TABLES.filter((t) => !existing.has(t));
@@ -25,15 +25,19 @@ const STATUS_TRANSITIONS = Object.freeze({
25
25
 
26
26
  const TASK_FIELDS = Object.freeze([
27
27
  'id', 'title', 'type', 'priority', 'complexity', 'estimated_days', 'status',
28
- 'deps', 'files_modified', 'session_id', 'stale', 'complexity_hint',
29
- 'tag', 'trace_to', 'context_file', 'completion_commit', 'parent_id',
28
+ 'deps', 'files_modified', 'session_id', 'stale',
29
+ 'tag', 'trace_to', 'outcome', 'slice_kind', 'public_seam', 'verification_command',
30
+ 'acceptance_json', 'context_refs_json', 'docs_impact_json', 'ownership_json',
31
+ 'context_file', 'completion_commit', 'parent_id',
30
32
  'change_id',
31
33
  'created_at', 'updated_at',
32
34
  ]);
33
35
 
34
36
  const PATCHABLE_FIELDS = Object.freeze([
35
37
  'priority', 'complexity', 'estimated_days', 'deps', 'files_modified',
36
- 'session_id', 'stale', 'complexity_hint', 'tag', 'trace_to',
38
+ 'session_id', 'stale', 'tag', 'trace_to',
39
+ 'outcome', 'slice_kind', 'public_seam', 'verification_command',
40
+ 'acceptance', 'context_refs', 'docs_impact', 'ownership',
37
41
  'context_file', 'completion_commit',
38
42
  'change_id',
39
43
  ]);
@@ -89,7 +93,7 @@ function withRetry(fn, { attempts = 10, baseMs = 50, capMs = 2000 } = {}) {
89
93
  }
90
94
 
91
95
  function tx(db, fn) {
92
- return withRetry(() => db.transaction(fn)());
96
+ return withRetry(() => db.transaction(fn).immediate());
93
97
  }
94
98
 
95
99
  // ─── tasks ───────────────────────────────────────────────────────────────
@@ -97,17 +101,132 @@ function tx(db, fn) {
97
101
  function rowToTask(row) {
98
102
  if (!row) return null;
99
103
  const out = { ...row };
104
+ // Pre-13 databases may retain this retired Claude-specific column. It is
105
+ // intentionally ignored instead of leaking host/model assumptions into the
106
+ // current task contract or projections.
107
+ delete out.complexity_hint;
100
108
  for (const k of ['deps', 'files_modified']) {
101
109
  if (typeof out[k] === 'string') {
102
110
  try { out[k] = JSON.parse(out[k]); } catch { out[k] = null; }
103
111
  }
104
112
  }
113
+ const jsonFields = {
114
+ acceptance_json: ['acceptance', []],
115
+ context_refs_json: ['context_refs', []],
116
+ docs_impact_json: ['docs_impact', { status: 'unknown', files: [], rationale: null }],
117
+ ownership_json: ['ownership', {}],
118
+ };
119
+ for (const [column, [field, fallback]] of Object.entries(jsonFields)) {
120
+ try { out[field] = typeof out[column] === 'string' ? JSON.parse(out[column]) : fallback; }
121
+ catch { out[field] = fallback; }
122
+ delete out[column];
123
+ }
105
124
  if (out.stale !== undefined && out.stale !== null) {
106
125
  out.stale = Boolean(out.stale);
107
126
  }
108
127
  return out;
109
128
  }
110
129
 
130
+ function normalizeObjectArray(value, field) {
131
+ if (!Array.isArray(value) || value.some((item) => !item || typeof item !== 'object' || Array.isArray(item))) {
132
+ throw new StateOpsError('VALIDATION_ERROR', `${field} must be an array of objects`);
133
+ }
134
+ return JSON.parse(JSON.stringify(value));
135
+ }
136
+
137
+ function normalizeAcceptance(value) {
138
+ const items = normalizeObjectArray(value, 'acceptance');
139
+ const ids = new Set();
140
+ return items.map((item, index) => {
141
+ const id = String(item.id || '').trim();
142
+ const criterion = String(item.criterion || '').trim();
143
+ const verification = String(item.verification || '').trim();
144
+ if (!id || !criterion || !verification || ids.has(id)) {
145
+ throw new StateOpsError(
146
+ 'VALIDATION_ERROR', `acceptance[${index}] requires a unique id, criterion, and verification`,
147
+ );
148
+ }
149
+ ids.add(id);
150
+ return { id, criterion, verification };
151
+ });
152
+ }
153
+
154
+ function normalizeContextRefs(value) {
155
+ return normalizeObjectArray(value, 'context_refs').map((item, index) => {
156
+ const ref = String(item.ref || '').trim();
157
+ const reason = String(item.reason || '').trim();
158
+ if (!ref || !reason) {
159
+ throw new StateOpsError('VALIDATION_ERROR', `context_refs[${index}] requires ref and reason`);
160
+ }
161
+ return {
162
+ ref, reason, kind: String(item.kind || 'source').trim(), required: item.required !== false,
163
+ };
164
+ });
165
+ }
166
+
167
+ function normalizeDocsImpact(value) {
168
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
169
+ throw new StateOpsError('VALIDATION_ERROR', 'docs_impact must be an object');
170
+ }
171
+ const status = String(value.status || 'unknown').trim();
172
+ if (!['unknown', 'required', 'none'].includes(status)) {
173
+ throw new StateOpsError('VALIDATION_ERROR', 'docs_impact.status must be unknown, required, or none');
174
+ }
175
+ const files = value.files === undefined ? [] : value.files;
176
+ if (!Array.isArray(files) || files.some((item) => typeof item !== 'string' || !item.trim())) {
177
+ throw new StateOpsError('VALIDATION_ERROR', 'docs_impact.files must be an array of paths');
178
+ }
179
+ return {
180
+ status, files: files.map((item) => item.trim()),
181
+ rationale: value.rationale == null ? null : String(value.rationale).trim(),
182
+ };
183
+ }
184
+
185
+ function normalizeOwnership(value) {
186
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
187
+ throw new StateOpsError('VALIDATION_ERROR', 'ownership must be an object');
188
+ }
189
+ const reviewers = value.reviewers === undefined ? [] : value.reviewers;
190
+ if (!Array.isArray(reviewers) || reviewers.some((item) => typeof item !== 'string' || !item.trim())) {
191
+ throw new StateOpsError('VALIDATION_ERROR', 'ownership.reviewers must be an array of names');
192
+ }
193
+ return {
194
+ owner: value.owner == null ? null : String(value.owner).trim(),
195
+ reviewers: reviewers.map((item) => item.trim()),
196
+ };
197
+ }
198
+
199
+ function taskContractBlockers(task) {
200
+ const blockers = [];
201
+ if (!String(task?.outcome || '').trim()) blockers.push('TASK_OUTCOME_MISSING');
202
+ if (!['tracer_bullet', 'expand_contract', 'integration_checkpoint'].includes(task?.slice_kind)) {
203
+ blockers.push('TASK_SLICE_KIND_MISSING');
204
+ }
205
+ if (!String(task?.public_seam || '').trim()) blockers.push('TASK_PUBLIC_SEAM_MISSING');
206
+ if (!String(task?.verification_command || '').trim()) blockers.push('TASK_VERIFICATION_COMMAND_MISSING');
207
+ if (!Array.isArray(task?.acceptance) || task.acceptance.length === 0) blockers.push('TASK_ACCEPTANCE_MISSING');
208
+ if (!Array.isArray(task?.context_refs) || task.context_refs.length === 0) blockers.push('TASK_CONTEXT_REFS_MISSING');
209
+ if (!String(task?.trace_to || '').trim()) blockers.push('TASK_TRACEABILITY_MISSING');
210
+ if (!task?.docs_impact || task.docs_impact.status === 'unknown') blockers.push('TASK_DOCS_IMPACT_UNRESOLVED');
211
+ else if (task.docs_impact.status === 'required' && task.docs_impact.files.length === 0) {
212
+ blockers.push('TASK_DOCS_FILES_MISSING');
213
+ } else if (!String(task.docs_impact.rationale || '').trim()) blockers.push('TASK_DOCS_RATIONALE_MISSING');
214
+ if (!String(task?.ownership?.owner || '').trim()) blockers.push('TASK_OWNER_MISSING');
215
+ return blockers;
216
+ }
217
+
218
+ function assertTaskExecutionContract(task) {
219
+ const blockers = taskContractBlockers(task);
220
+ if (blockers.length > 0) {
221
+ throw new StateOpsError(
222
+ 'TASK_EXECUTION_CONTRACT_INCOMPLETE',
223
+ `task ${task?.id || '(unknown)'} execution contract is incomplete`,
224
+ { details: { blockers } },
225
+ );
226
+ }
227
+ return task;
228
+ }
229
+
111
230
  function readTask(db, id) {
112
231
  const row = db.prepare('SELECT * FROM tasks WHERE id = ?').get(id);
113
232
  return rowToTask(row);
@@ -116,13 +235,14 @@ function readTask(db, id) {
116
235
  // Single static SQL with named parameters and NULL-pass-through filters.
117
236
  // Every value flows through @bindings; the string is a frozen literal so no
118
237
  // concatenation / interpolation hooks can flag injection risk.
119
- const LIST_TASKS_SQL = "SELECT * FROM tasks WHERE (@status IS NULL OR status = @status) AND (@tag IS NULL OR tag = @tag) AND (@since IS NULL OR updated_at >= @since) ORDER BY created_at ASC LIMIT IIF(@maxn IS NULL, -1, @maxn)";
238
+ const LIST_TASKS_SQL = "SELECT * FROM tasks WHERE (@status IS NULL OR status = @status) AND (@tag IS NULL OR tag = @tag) AND (@change_id IS NULL OR change_id = @change_id) AND (@since IS NULL OR updated_at >= @since) ORDER BY created_at ASC LIMIT IIF(@maxn IS NULL, -1, @maxn)";
120
239
 
121
240
  function listTasks(db, filter = {}) {
122
241
  const status = filter.status && filter.status !== 'any' ? filter.status : null;
123
242
  return db.prepare(LIST_TASKS_SQL).all({
124
243
  status,
125
244
  tag: filter.tag || null,
245
+ change_id: filter.change_id || null,
126
246
  since: filter.since || null,
127
247
  maxn: filter.limit || null,
128
248
  }).map(rowToTask);
@@ -203,9 +323,18 @@ function createTask(db, input) {
203
323
  files_modified: input.files_modified ? JSON.stringify(input.files_modified) : null,
204
324
  session_id: input.session_id ?? null,
205
325
  stale: 0,
206
- complexity_hint: input.complexity_hint ?? null,
207
326
  tag: input.tag ?? derivedTag,
208
327
  trace_to: input.trace_to ?? null,
328
+ outcome: input.outcome ?? null,
329
+ slice_kind: input.slice_kind ?? null,
330
+ public_seam: input.public_seam ?? null,
331
+ verification_command: input.verification_command ?? null,
332
+ acceptance_json: JSON.stringify(input.acceptance === undefined ? [] : normalizeAcceptance(input.acceptance)),
333
+ context_refs_json: JSON.stringify(input.context_refs === undefined ? [] : normalizeContextRefs(input.context_refs)),
334
+ docs_impact_json: JSON.stringify(input.docs_impact === undefined
335
+ ? { status: 'unknown', files: [], rationale: null }
336
+ : normalizeDocsImpact(input.docs_impact)),
337
+ ownership_json: JSON.stringify(input.ownership === undefined ? {} : normalizeOwnership(input.ownership)),
209
338
  context_file: input.context_file ?? null,
210
339
  completion_commit: null,
211
340
  parent_id: input.parent_id ?? null,
@@ -271,6 +400,26 @@ function patchTask(db, id, patch = {}) {
271
400
  }
272
401
  value = value === null ? null : JSON.stringify(value);
273
402
  }
403
+ if (key === 'acceptance') {
404
+ sets.push('acceptance_json = ?');
405
+ params.push(JSON.stringify(normalizeAcceptance(value)));
406
+ continue;
407
+ }
408
+ if (key === 'context_refs') {
409
+ sets.push('context_refs_json = ?');
410
+ params.push(JSON.stringify(normalizeContextRefs(value)));
411
+ continue;
412
+ }
413
+ if (key === 'docs_impact') {
414
+ sets.push('docs_impact_json = ?');
415
+ params.push(JSON.stringify(normalizeDocsImpact(value)));
416
+ continue;
417
+ }
418
+ if (key === 'ownership') {
419
+ sets.push('ownership_json = ?');
420
+ params.push(JSON.stringify(normalizeOwnership(value)));
421
+ continue;
422
+ }
274
423
  if (key === 'stale') value = value ? 1 : 0;
275
424
  sets.push(`${key} = ?`);
276
425
  params.push(value);
@@ -709,11 +858,11 @@ function markTasksStaleBySpecSections(db, sections) {
709
858
  // shows up under a synthetic "unknown" runtime bucket. Writes go through
710
859
  // mcp-server/lib/telemetry.cjs — don't insert telemetry rows directly.
711
860
 
712
- const AGGREGATE_BY_RUNTIME_SQL = "SELECT COALESCE(s.runtime, 'unknown') AS runtime, COUNT(*) AS calls, COALESCE(SUM(t.tokens_input), 0) AS tokens_in, COALESCE(SUM(t.tokens_output), 0) AS tokens_out, COALESCE(SUM(t.cost_usd), 0.0) AS cost_usd FROM telemetry t LEFT JOIN sessions s ON t.session_id = s.sid WHERE (@since IS NULL OR t.ts >= @since) GROUP BY COALESCE(s.runtime, 'unknown') ORDER BY cost_usd DESC";
861
+ const AGGREGATE_BY_RUNTIME_SQL = "SELECT COALESCE(s.runtime, 'unknown') AS runtime, COUNT(*) AS calls, COALESCE(SUM(t.tokens_input), 0) AS tokens_in, COALESCE(SUM(t.tokens_output), 0) AS tokens_out, COALESCE(SUM(t.cost_usd), 0.0) AS cost_usd, SUM(CASE WHEN t.event_type = 'token_usage' AND (COALESCE(t.tokens_input, 0) > 0 OR COALESCE(t.tokens_output, 0) > 0) AND t.cost_usd IS NULL THEN 1 ELSE 0 END) AS unpriced_usage_events, SUM(CASE WHEN t.event_type = 'token_usage' AND (COALESCE(t.tokens_input, 0) > 0 OR COALESCE(t.tokens_output, 0) > 0) AND t.cost_usd IS NOT NULL THEN 1 ELSE 0 END) AS priced_usage_events FROM telemetry t LEFT JOIN sessions s ON t.session_id = s.sid WHERE (@since IS NULL OR t.ts >= @since) GROUP BY COALESCE(s.runtime, 'unknown') ORDER BY cost_usd DESC";
713
862
 
714
- const AGGREGATE_BY_TASK_SQL = "SELECT s.task_id AS task_id, COUNT(*) AS calls, COALESCE(SUM(t.tokens_input), 0) AS tokens_in, COALESCE(SUM(t.tokens_output), 0) AS tokens_out, COALESCE(SUM(t.cost_usd), 0.0) AS cost_usd FROM telemetry t JOIN sessions s ON t.session_id = s.sid WHERE s.task_id IS NOT NULL AND (@since IS NULL OR t.ts >= @since) GROUP BY s.task_id ORDER BY cost_usd DESC LIMIT @maxn";
863
+ const AGGREGATE_BY_TASK_SQL = "SELECT s.task_id AS task_id, COUNT(*) AS calls, COALESCE(SUM(t.tokens_input), 0) AS tokens_in, COALESCE(SUM(t.tokens_output), 0) AS tokens_out, COALESCE(SUM(t.cost_usd), 0.0) AS cost_usd, SUM(CASE WHEN t.event_type = 'token_usage' AND (COALESCE(t.tokens_input, 0) > 0 OR COALESCE(t.tokens_output, 0) > 0) AND t.cost_usd IS NULL THEN 1 ELSE 0 END) AS unpriced_usage_events, SUM(CASE WHEN t.event_type = 'token_usage' AND (COALESCE(t.tokens_input, 0) > 0 OR COALESCE(t.tokens_output, 0) > 0) AND t.cost_usd IS NOT NULL THEN 1 ELSE 0 END) AS priced_usage_events FROM telemetry t JOIN sessions s ON t.session_id = s.sid WHERE s.task_id IS NOT NULL AND (@since IS NULL OR t.ts >= @since) GROUP BY s.task_id ORDER BY cost_usd DESC LIMIT @maxn";
715
864
 
716
- const AGGREGATE_BY_SESSION_SQL = "SELECT @sid AS session_id, COUNT(*) AS tool_calls, COALESCE(SUM(tokens_input), 0) AS tokens_in, COALESCE(SUM(tokens_output), 0) AS tokens_out, COALESCE(SUM(cost_usd), 0.0) AS cost_usd FROM telemetry WHERE session_id = @sid";
865
+ const AGGREGATE_BY_SESSION_SQL = "SELECT @sid AS session_id, COUNT(*) AS tool_calls, COALESCE(SUM(tokens_input), 0) AS tokens_in, COALESCE(SUM(tokens_output), 0) AS tokens_out, COALESCE(SUM(cost_usd), 0.0) AS cost_usd, SUM(CASE WHEN event_type = 'token_usage' AND (COALESCE(tokens_input, 0) > 0 OR COALESCE(tokens_output, 0) > 0) AND cost_usd IS NULL THEN 1 ELSE 0 END) AS unpriced_usage_events, SUM(CASE WHEN event_type = 'token_usage' AND (COALESCE(tokens_input, 0) > 0 OR COALESCE(tokens_output, 0) > 0) AND cost_usd IS NOT NULL THEN 1 ELSE 0 END) AS priced_usage_events FROM telemetry WHERE session_id = @sid";
717
866
 
718
867
  function aggregateTelemetryByRuntime(db, { since = null } = {}) {
719
868
  return db.prepare(AGGREGATE_BY_RUNTIME_SQL).all({ since });
@@ -765,6 +914,8 @@ module.exports = {
765
914
  StateOpsError,
766
915
  STATUS_TRANSITIONS,
767
916
  PATCHABLE_FIELDS,
917
+ taskContractBlockers,
918
+ assertTaskExecutionContract,
768
919
  SESSION_PATCHABLE,
769
920
  openStateDb,
770
921
  tx,