ultra-builder-pro-cli 0.13.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 (172) 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 +86 -1
  8. package/README.md +40 -26
  9. package/adapters/_shared/codex-assets.cjs +29 -165
  10. package/adapters/_shared/file-ops.cjs +6 -2
  11. package/adapters/_shared/runtime-assets.cjs +1 -0
  12. package/adapters/claude.js +3 -2
  13. package/adapters/codex.js +1 -0
  14. package/adapters/kimi.js +3 -2
  15. package/adapters/opencode.js +33 -122
  16. package/agents/review-coordinator.md +9 -6
  17. package/docs/AGENT-CONTEXT.md +21 -11
  18. package/docs/ARCHITECTURE.md +41 -13
  19. package/docs/PLAN.zh-CN.md +3 -2
  20. package/docs/ROADMAP.md +9 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
  22. package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
  23. package/docs/WORKFLOW-LIFECYCLE.md +266 -0
  24. package/hooks/README.md +6 -1
  25. package/hooks/active_task_context.py +22 -50
  26. package/hooks/context_spine.py +61 -332
  27. package/hooks/health_check.py +29 -18
  28. package/hooks/pre_stop_check.py +28 -33
  29. package/hooks/subagent_tracker.py +54 -85
  30. package/hooks/workflow_checkpoint.py +39 -47
  31. package/hooks/workflow_context.py +4 -9
  32. package/hooks/workflow_resume.py +48 -128
  33. package/mcp-server/breadcrumb.cjs +22 -0
  34. package/mcp-server/hook-event.cjs +34 -0
  35. package/mcp-server/lib/archive-journal.cjs +17 -5
  36. package/mcp-server/lib/baseline-workflow.cjs +158 -18
  37. package/mcp-server/lib/change-workflow.cjs +550 -85
  38. package/mcp-server/lib/context-spine.cjs +242 -24
  39. package/mcp-server/lib/doctor.cjs +33 -3
  40. package/mcp-server/lib/init-project.cjs +210 -16
  41. package/mcp-server/lib/plan-store.cjs +4 -1
  42. package/mcp-server/lib/prd-parser.cjs +14 -41
  43. package/mcp-server/lib/pricing.cjs +7 -10
  44. package/mcp-server/lib/project-breadcrumb.cjs +145 -0
  45. package/mcp-server/lib/projector.cjs +57 -3
  46. package/mcp-server/lib/spec-learning.cjs +79 -2
  47. package/mcp-server/lib/state-db.cjs +145 -8
  48. package/mcp-server/lib/state-ops.cjs +160 -9
  49. package/mcp-server/lib/task-expander.cjs +9 -38
  50. package/mcp-server/lib/workflow-state.cjs +2292 -0
  51. package/mcp-server/server.cjs +182 -50
  52. package/orchestrator/daemon.cjs +1 -2
  53. package/orchestrator/dispatch-rules.cjs +5 -31
  54. package/orchestrator/planner/plan-builder.cjs +21 -12
  55. package/package.json +14 -6
  56. package/skills/cc-collab/SKILL.md +2 -2
  57. package/skills/codex-collab/SKILL.md +2 -3
  58. package/skills/ultra-change/SKILL.md +53 -68
  59. package/skills/ultra-change/references/change-contract.md +36 -0
  60. package/skills/ultra-deliver/SKILL.md +69 -79
  61. package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
  62. package/skills/ultra-dev/SKILL.md +54 -59
  63. package/skills/ultra-doctor/SKILL.md +40 -71
  64. package/skills/ultra-init/SKILL.md +96 -103
  65. package/skills/ultra-init/references/brownfield-adoption.md +2 -2
  66. package/skills/ultra-plan/SKILL.md +79 -75
  67. package/skills/ultra-plan/references/semantic-preflight.md +18 -0
  68. package/skills/ultra-research/SKILL.md +87 -60
  69. package/skills/ultra-research/references/00-problem-validation.md +36 -0
  70. package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
  71. package/skills/ultra-research/references/02-market-assessment.md +30 -0
  72. package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
  73. package/skills/ultra-research/references/04-product-strategy.md +32 -0
  74. package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
  75. package/skills/ultra-research/references/10-user-personas.md +29 -0
  76. package/skills/ultra-research/references/11-user-scenarios.md +28 -0
  77. package/skills/ultra-research/references/20-user-stories.md +30 -0
  78. package/skills/ultra-research/references/21-features-scope.md +29 -0
  79. package/skills/ultra-research/references/22-success-metrics.md +28 -0
  80. package/skills/ultra-research/references/30-architecture-context.md +30 -0
  81. package/skills/ultra-research/references/31-solution-strategy.md +28 -0
  82. package/skills/ultra-research/references/32-building-blocks.md +28 -0
  83. package/skills/ultra-research/references/40-deployment.md +29 -0
  84. package/skills/ultra-research/references/41-quality-risks.md +28 -0
  85. package/skills/ultra-research/references/99-synthesis.md +35 -0
  86. package/skills/ultra-review/SKILL.md +57 -73
  87. package/skills/ultra-review/references/review-modes.md +27 -0
  88. package/skills/ultra-review/references/unified-schema.md +25 -0
  89. package/skills/ultra-review/scripts/review_wait.py +40 -3
  90. package/skills/ultra-status/SKILL.md +41 -45
  91. package/skills/ultra-test/SKILL.md +48 -55
  92. package/skills/ultra-verify/SKILL.md +22 -31
  93. package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
  94. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  95. package/spec/cli-protocol.md +7 -2
  96. package/spec/mcp-tools.yaml +422 -73
  97. package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
  98. package/spec/schemas/delivery-report.v1.schema.json +111 -0
  99. package/spec/schemas/mcp-tools.schema.json +1 -1
  100. package/spec/schemas/state-db.sql +82 -1
  101. package/spec/schemas/tasks.v4.5.schema.json +8 -1
  102. package/spec/schemas/test-report.v1.schema.json +194 -0
  103. package/ultra-tools/cli.cjs +1 -1
  104. package/ultra-tools/commands/migrate.cjs +3 -3
  105. package/ultra-tools/commands/status.cjs +213 -12
  106. package/.ultra-template/delivery-report.json +0 -9
  107. package/.ultra-template/test-report.json +0 -8
  108. package/adapters/_shared/tests/file-ops.test.cjs +0 -54
  109. package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
  110. package/adapters/_shared/tests/handbook.test.cjs +0 -101
  111. package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
  112. package/adapters/_shared/tests/provenance.test.cjs +0 -133
  113. package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
  114. package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
  115. package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
  116. package/adapters/_shared/tests/validate.test.cjs +0 -30
  117. package/adapters/tests/claude.test.cjs +0 -137
  118. package/adapters/tests/codex-hook.test.cjs +0 -115
  119. package/adapters/tests/codex.test.cjs +0 -423
  120. package/adapters/tests/kimi-hook.test.cjs +0 -122
  121. package/adapters/tests/kimi.test.cjs +0 -259
  122. package/adapters/tests/opencode.test.cjs +0 -371
  123. package/mcp-server/lib/archive-journal.test.cjs +0 -35
  124. package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
  125. package/mcp-server/lib/change-workflow.test.cjs +0 -971
  126. package/mcp-server/lib/context-spine.test.cjs +0 -93
  127. package/mcp-server/lib/doctor.test.cjs +0 -148
  128. package/mcp-server/lib/llm-client.cjs +0 -157
  129. package/mcp-server/lib/llm-client.test.cjs +0 -156
  130. package/mcp-server/lib/plan-store.test.cjs +0 -132
  131. package/mcp-server/lib/prd-parser.test.cjs +0 -171
  132. package/mcp-server/lib/runtime-state.test.cjs +0 -92
  133. package/mcp-server/lib/spec-learning.test.cjs +0 -67
  134. package/mcp-server/lib/state-db.test.cjs +0 -314
  135. package/mcp-server/lib/task-expander.test.cjs +0 -324
  136. package/mcp-server/lib/topo.test.cjs +0 -158
  137. package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
  138. package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
  139. package/mcp-server/tests/concurrency-update.test.cjs +0 -96
  140. package/mcp-server/tests/concurrency.test.cjs +0 -112
  141. package/mcp-server/tests/db-ops.test.cjs +0 -121
  142. package/mcp-server/tests/init-project.test.cjs +0 -393
  143. package/mcp-server/tests/migrate.test.cjs +0 -232
  144. package/mcp-server/tests/projector.test.cjs +0 -162
  145. package/mcp-server/tests/server-contract.test.cjs +0 -1471
  146. package/mcp-server/tests/staleness.test.cjs +0 -190
  147. package/mcp-server/tests/state-db.test.cjs +0 -106
  148. package/mcp-server/tests/state-ops.test.cjs +0 -449
  149. package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
  150. package/mcp-server/tests/telemetry.test.cjs +0 -219
  151. package/orchestrator/planner/expand-prompt.cjs +0 -62
  152. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  153. package/orchestrator/planner/plan-builder.test.cjs +0 -165
  154. package/orchestrator/planner/prd-prompt.cjs +0 -44
  155. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
  156. package/orchestrator/tests/auto-merge.test.cjs +0 -216
  157. package/orchestrator/tests/daemon.test.cjs +0 -311
  158. package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
  159. package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
  160. package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
  161. package/orchestrator/tests/recovery.test.cjs +0 -190
  162. package/orchestrator/tests/session-runner.test.cjs +0 -314
  163. package/orchestrator/tests/skill-miner.test.cjs +0 -147
  164. package/orchestrator/tests/worktree-manager.test.cjs +0 -148
  165. package/skills/ultra-research/references/architecture.md +0 -38
  166. package/skills/ultra-research/references/problem-product.md +0 -39
  167. package/skills/ultra-research/references/quality-delivery.md +0 -34
  168. package/skills/ultra-research/references/synthesis.md +0 -38
  169. package/skills/ultra-research/references/users-scope.md +0 -40
  170. package/ultra-tools/legacy-memory.test.cjs +0 -80
  171. package/ultra-tools/status-command.test.cjs +0 -181
  172. package/ultra-tools/system-command.test.cjs +0 -309
@@ -16,8 +16,11 @@ const SCHEMA_VERSION = '4.5';
16
16
  const SOURCE_TAG = '.ultra/state.db';
17
17
 
18
18
  // Frozen SELECTs — values bind through @placeholders.
19
- const LIST_TASKS_FOR_PROJECTION_SQL = "SELECT id, title, type, priority, complexity, estimated_days, status, deps, files_modified, session_id, stale, complexity_hint, tag, trace_to, context_file, completion_commit, created_at, updated_at FROM tasks ORDER BY created_at ASC";
20
- const READ_TASK_FOR_PROJECTION_SQL = "SELECT id, title, type, priority, complexity, estimated_days, status, deps, files_modified, session_id, stale, complexity_hint, tag, trace_to, context_file, completion_commit, created_at, updated_at FROM tasks WHERE id = @id";
19
+ const TASK_PROJECTION_COLUMNS = 'id, title, type, priority, complexity, estimated_days, status, deps, files_modified, session_id, stale, tag, trace_to, outcome, slice_kind, public_seam, verification_command, acceptance_json, context_refs_json, docs_impact_json, ownership_json, context_file, completion_commit, created_at, updated_at';
20
+ const LIST_TASKS_FOR_PROJECTION_SQL = `SELECT ${TASK_PROJECTION_COLUMNS} FROM tasks ORDER BY created_at ASC`;
21
+ const READ_TASK_FOR_PROJECTION_SQL = `SELECT ${TASK_PROJECTION_COLUMNS} FROM tasks WHERE id = @id`;
22
+ const CONTRACT_START = '<!-- ultra:task-contract:start -->';
23
+ const CONTRACT_END = '<!-- ultra:task-contract:end -->';
21
24
 
22
25
  function rowToProjection(row) {
23
26
  if (!row) return null;
@@ -28,6 +31,17 @@ function rowToProjection(row) {
28
31
  }
29
32
  if (out[k] === null || out[k] === undefined) delete out[k];
30
33
  }
34
+ const jsonFields = {
35
+ acceptance_json: ['acceptance', []],
36
+ context_refs_json: ['context_refs', []],
37
+ docs_impact_json: ['docs_impact', { status: 'unknown', files: [], rationale: null }],
38
+ ownership_json: ['ownership', {}],
39
+ };
40
+ for (const [column, [field, fallback]] of Object.entries(jsonFields)) {
41
+ try { out[field] = typeof out[column] === 'string' ? JSON.parse(out[column]) : fallback; }
42
+ catch { out[field] = fallback; }
43
+ delete out[column];
44
+ }
31
45
  if (out.stale !== undefined && out.stale !== null) out.stale = Boolean(out.stale);
32
46
  for (const k of Object.keys(out)) {
33
47
  if (out[k] === null || out[k] === undefined) delete out[k];
@@ -94,17 +108,57 @@ function buildContextDoc(taskRow, existingBody) {
94
108
  body = body.replace(/^>\s*\*\*Status\*\*:.*(?:\r?\n|$)(?:\r?\n)?/mi, '');
95
109
  // Strip any previous STALE banner so re-projection after reset is clean.
96
110
  body = body.replace(/^> ⚠️\s*STALE[^\n]*\n(?:>[^\n]*\n)*\n?/m, '');
111
+ body = body.replace(
112
+ new RegExp(`${CONTRACT_START}[\\s\\S]*?${CONTRACT_END}\\n*`, 'm'),
113
+ '',
114
+ );
97
115
  if (taskRow.stale) {
98
116
  const banner = [
99
117
  `> ⚠️ STALE since ${taskRow.updated_at || new Date().toISOString()}`,
100
- '> spec 改动可能让此 task 上下文失效;重新跑 spec 对齐后再继续。',
118
+ '> Specification evidence may have changed. Recompile the task context before continuing.',
101
119
  '',
102
120
  ].join('\n');
103
121
  body = banner + body;
104
122
  }
123
+ body = buildTaskContract(taskRow) + body;
105
124
  return headerLines.join('\n') + body;
106
125
  }
107
126
 
127
+ function buildTaskContract(task) {
128
+ const acceptance = Array.isArray(task.acceptance) ? task.acceptance : [];
129
+ const refs = Array.isArray(task.context_refs) ? task.context_refs : [];
130
+ const docs = task.docs_impact || { status: 'unknown', files: [], rationale: null };
131
+ const ownership = task.ownership || {};
132
+ const lines = [
133
+ CONTRACT_START,
134
+ '## Execution Contract (generated from state.db)',
135
+ '',
136
+ `- Outcome: ${task.outcome || 'unresolved'}`,
137
+ `- Slice: ${task.slice_kind || 'unresolved'}`,
138
+ `- Public seam: ${task.public_seam || 'unresolved'}`,
139
+ `- Verification: ${task.verification_command || 'unresolved'}`,
140
+ `- Trace: ${task.trace_to || 'unresolved'}`,
141
+ `- Owner: ${ownership.owner || 'unresolved'}`,
142
+ `- Documentation: ${docs.status || 'unknown'}${docs.rationale ? ` — ${docs.rationale}` : ''}`,
143
+ '',
144
+ '### Acceptance',
145
+ '',
146
+ ...(acceptance.length > 0
147
+ ? acceptance.map((item) => `- ${item.id}: ${item.criterion} — verify with \`${item.verification}\``)
148
+ : ['- unresolved']),
149
+ '',
150
+ '### Context References',
151
+ '',
152
+ ...(refs.length > 0
153
+ ? refs.map((item) => `- \`${item.ref}\` — ${item.reason}`)
154
+ : ['- unresolved']),
155
+ '',
156
+ CONTRACT_END,
157
+ '',
158
+ ];
159
+ return lines.join('\n');
160
+ }
161
+
108
162
  function escapeYaml(str) {
109
163
  if (str == null) return '';
110
164
  if (/[:#\[\]\{\}&\*!|>'"%@`]/.test(str) || /^\s|\s$/.test(str)) {
@@ -33,8 +33,14 @@ function rowToCandidate(row) {
33
33
  catch (error) {
34
34
  throw new SpecLearningError('STATE_CORRUPT', `invalid spec-learning evidence: ${error.message}`);
35
35
  }
36
- const candidate = { ...row, evidence };
36
+ let applyEvidence;
37
+ try { applyEvidence = JSON.parse(row.apply_evidence_json || '[]'); }
38
+ catch (error) {
39
+ throw new SpecLearningError('STATE_CORRUPT', `invalid spec-learning apply evidence: ${error.message}`);
40
+ }
41
+ const candidate = { ...row, evidence, apply_evidence: applyEvidence };
37
42
  delete candidate.evidence_json;
43
+ delete candidate.apply_evidence_json;
38
44
  return candidate;
39
45
  }
40
46
 
@@ -102,6 +108,66 @@ function normalizeEvidence(value) {
102
108
  return [...new Set(value.map((entry) => entry.trim()))];
103
109
  }
104
110
 
111
+ function targetFile(rootDir, targetRef) {
112
+ const separator = String(targetRef || '').lastIndexOf('#');
113
+ if (separator <= 0 || separator === targetRef.length - 1) {
114
+ throw new SpecLearningError('VALIDATION_ERROR', 'target_ref must use project-relative path#anchor');
115
+ }
116
+ const relative = targetRef.slice(0, separator);
117
+ if (path.isAbsolute(relative)) {
118
+ throw new SpecLearningError('VALIDATION_ERROR', 'target_ref must be project-relative');
119
+ }
120
+ const root = path.resolve(rootDir);
121
+ const file = path.resolve(root, relative);
122
+ if (file !== root && !file.startsWith(`${root}${path.sep}`)) {
123
+ throw new SpecLearningError('VALIDATION_ERROR', 'target_ref escapes the project root');
124
+ }
125
+ if (!fs.existsSync(file) || !fs.statSync(file).isFile()) {
126
+ throw new SpecLearningError('LEARNING_TARGET_MISSING', `target file does not exist: ${relative}`);
127
+ }
128
+ return { file, relative };
129
+ }
130
+
131
+ function digestFile(file) {
132
+ return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
133
+ }
134
+
135
+ function verifyApplyEvidence(input, current, rootDir) {
136
+ const appliedRef = typeof input.applied_ref === 'string' ? input.applied_ref.trim() : '';
137
+ const beforeDigest = typeof input.before_digest === 'string' ? input.before_digest.trim() : '';
138
+ const afterDigest = typeof input.after_digest === 'string' ? input.after_digest.trim() : '';
139
+ const evidence = normalizeEvidence(input.apply_evidence);
140
+ if (appliedRef !== current.target_ref || !/^[0-9a-f]{64}$/.test(beforeDigest)
141
+ || !/^[0-9a-f]{64}$/.test(afterDigest) || beforeDigest === afterDigest
142
+ || evidence.length === 0) {
143
+ throw new SpecLearningError(
144
+ 'LEARNING_APPLY_EVIDENCE_REQUIRED',
145
+ 'apply requires the exact target ref, distinct before/after digests, and durable evidence refs',
146
+ );
147
+ }
148
+ if (current.before_digest && current.before_digest !== beforeDigest) {
149
+ throw new SpecLearningError(
150
+ 'LEARNING_APPLY_EVIDENCE_MISMATCH', 'before_digest does not match the proposed target state',
151
+ );
152
+ }
153
+ const target = targetFile(rootDir, appliedRef);
154
+ if (digestFile(target.file) !== afterDigest) {
155
+ throw new SpecLearningError(
156
+ 'LEARNING_APPLY_EVIDENCE_MISMATCH', 'after_digest does not match the current target file',
157
+ );
158
+ }
159
+ const workflows = require('./workflow-state.cjs');
160
+ try {
161
+ workflows.resolveProjectSourceRef(rootDir, appliedRef, 'applied_ref');
162
+ for (const ref of evidence) workflows.resolveProjectSourceRef(rootDir, ref, 'apply_evidence');
163
+ } catch (error) {
164
+ throw new SpecLearningError(
165
+ 'LEARNING_APPLY_EVIDENCE_MISMATCH', `applied target or evidence anchor is invalid: ${error.message}`,
166
+ );
167
+ }
168
+ return { appliedRef, beforeDigest, afterDigest, evidence };
169
+ }
170
+
105
171
  function proposeSpecLearning(db, input, { rootDir = process.cwd() } = {}) {
106
172
  const change = mutableChange(db, input.change_id);
107
173
  const id = String(input.id || `learning-${crypto.randomUUID().slice(0, 12)}`).trim();
@@ -120,6 +186,8 @@ function proposeSpecLearning(db, input, { rootDir = process.cwd() } = {}) {
120
186
  }
121
187
  }
122
188
  const evidence = normalizeEvidence(input.evidence);
189
+ const proposedTarget = targetFile(rootDir, targetRef);
190
+ const proposedDigest = digestFile(proposedTarget.file);
123
191
  const proposedAt = nowIso();
124
192
  return ops.tx(db, () => {
125
193
  try {
@@ -128,6 +196,9 @@ function proposeSpecLearning(db, input, { rootDir = process.cwd() } = {}) {
128
196
  (id, change_id, task_id, target_ref, summary, evidence_json, proposed_at)
129
197
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
130
198
  ).run(id, change.id, input.task_id || null, targetRef, summary, JSON.stringify(evidence), proposedAt);
199
+ db.prepare(
200
+ 'UPDATE spec_learning_candidates SET before_digest = ? WHERE id = ? AND change_id = ?',
201
+ ).run(proposedDigest, id, change.id);
131
202
  } catch (error) {
132
203
  if (String(error.message).includes('UNIQUE')) {
133
204
  throw new SpecLearningError('DUPLICATE_LEARNING_ID', `spec-learning candidate ${id} exists`);
@@ -164,13 +235,19 @@ function resolveSpecLearning(db, input, { rootDir = process.cwd() } = {}) {
164
235
  throw new SpecLearningError('VALIDATION_ERROR', `${input.decision} requires a resolution`);
165
236
  }
166
237
  const timestamp = nowIso();
238
+ const application = status === 'applied'
239
+ ? verifyApplyEvidence(input, current, rootDir)
240
+ : null;
167
241
  return ops.tx(db, () => {
168
242
  db.prepare(
169
243
  `UPDATE spec_learning_candidates
170
- SET status = ?, resolution = ?, resolved_at = ?, applied_at = ?
244
+ SET status = ?, resolution = ?, resolved_at = ?, applied_at = ?, applied_ref = ?,
245
+ before_digest = COALESCE(?, before_digest), after_digest = ?, apply_evidence_json = ?
171
246
  WHERE id = ? AND change_id = ?`,
172
247
  ).run(
173
248
  status, resolution || null, timestamp, status === 'applied' ? timestamp : null,
249
+ application?.appliedRef || null, application?.beforeDigest || null,
250
+ application?.afterDigest || null, JSON.stringify(application?.evidence || []),
174
251
  current.id, change.id,
175
252
  );
176
253
  ops.appendEventInTx(db, {
@@ -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));