ultra-builder-pro-cli 0.10.0 → 0.12.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 (137) hide show
  1. package/.ultra-template/specs/architecture.md +54 -223
  2. package/.ultra-template/specs/discovery.md +34 -231
  3. package/.ultra-template/specs/product.md +44 -200
  4. package/.ultra-template/tasks/contexts/TEMPLATE.md +36 -0
  5. package/.ultra-template/tasks/tasks.json +6 -0
  6. package/CHANGELOG.md +86 -1
  7. package/README.md +21 -9
  8. package/adapters/_shared/codex-assets.cjs +24 -342
  9. package/adapters/_shared/handbook.cjs +4 -3
  10. package/adapters/_shared/runtime-assets.cjs +24 -0
  11. package/adapters/_shared/tests/handbook.test.cjs +4 -0
  12. package/adapters/_shared/tests/runtime-assets.test.cjs +11 -4
  13. package/adapters/claude.js +28 -5
  14. package/adapters/kimi.js +15 -138
  15. package/adapters/opencode.js +22 -49
  16. package/adapters/tests/claude.test.cjs +22 -8
  17. package/adapters/tests/codex.test.cjs +21 -8
  18. package/adapters/tests/kimi-hook.test.cjs +1 -1
  19. package/adapters/tests/kimi.test.cjs +11 -11
  20. package/adapters/tests/opencode.test.cjs +47 -8
  21. package/agents/code-reviewer.md +24 -85
  22. package/agents/debugger.md +30 -132
  23. package/agents/review-code.md +24 -104
  24. package/agents/review-comments.md +20 -83
  25. package/agents/review-coordinator.md +40 -137
  26. package/agents/review-design.md +21 -118
  27. package/agents/review-errors.md +22 -70
  28. package/agents/review-spec.md +36 -0
  29. package/agents/review-tests.md +20 -76
  30. package/agents/tdd-runner.md +26 -84
  31. package/commands/learn.md +5 -36
  32. package/commands/ultra-change.md +5 -43
  33. package/commands/ultra-deliver.md +4 -46
  34. package/commands/ultra-dev.md +4 -48
  35. package/commands/ultra-doctor.md +4 -30
  36. package/commands/ultra-init.md +5 -47
  37. package/commands/ultra-plan.md +5 -48
  38. package/commands/ultra-research.md +5 -54
  39. package/commands/ultra-status.md +5 -44
  40. package/commands/ultra-test.md +5 -45
  41. package/commands/ultra-think.md +5 -40
  42. package/docs/AGENT-CONTEXT.md +16 -8
  43. package/docs/ARCHITECTURE.md +32 -5
  44. package/docs/PLAN.zh-CN.md +8 -6
  45. package/docs/ROADMAP.md +5 -3
  46. package/docs/RUNTIME-COMPAT-MATRIX.md +8 -5
  47. package/docs/SKILL-AUTHORING.md +75 -0
  48. package/docs/STATE-DB-ACCESS-POLICY.md +17 -11
  49. package/docs/USER-HANDBOOK-CONTRACT.md +8 -5
  50. package/hooks/README.md +4 -3
  51. package/hooks/adapters/codex.py +1 -1
  52. package/hooks/adapters/kimi.py +1 -1
  53. package/hooks/context_spine.py +120 -16
  54. package/hooks/health_check.py +11 -0
  55. package/hooks/pre_stop_check.py +6 -5
  56. package/mcp-server/lib/baseline-workflow.cjs +415 -0
  57. package/mcp-server/lib/baseline-workflow.test.cjs +225 -0
  58. package/mcp-server/lib/change-workflow.cjs +24 -27
  59. package/mcp-server/lib/change-workflow.test.cjs +202 -6
  60. package/mcp-server/lib/context-spine.cjs +74 -9
  61. package/mcp-server/lib/context-spine.test.cjs +15 -0
  62. package/mcp-server/lib/doctor.cjs +14 -0
  63. package/mcp-server/lib/doctor.test.cjs +25 -1
  64. package/mcp-server/lib/init-project.cjs +49 -14
  65. package/mcp-server/lib/provider-refs.cjs +42 -0
  66. package/mcp-server/lib/spec-learning.test.cjs +5 -0
  67. package/mcp-server/lib/state-db.cjs +63 -4
  68. package/mcp-server/lib/state-db.test.cjs +8 -3
  69. package/mcp-server/server.cjs +48 -3
  70. package/mcp-server/tests/init-project.test.cjs +54 -8
  71. package/mcp-server/tests/server-contract.test.cjs +99 -2
  72. package/mcp-server/tests/state-db.test.cjs +11 -2
  73. package/package.json +3 -2
  74. package/skills/cc-collab/SKILL.md +16 -18
  75. package/skills/code-review-expert/SKILL.md +39 -114
  76. package/skills/code-review-expert/references/correctness-reliability.md +30 -0
  77. package/skills/code-review-expert/references/design-boundaries.md +26 -0
  78. package/skills/code-review-expert/references/removal-plan.md +17 -45
  79. package/skills/codex-collab/SKILL.md +23 -28
  80. package/skills/codex-collab/evals/evals.json +18 -23
  81. package/skills/integration-rules/SKILL.md +28 -67
  82. package/skills/learn/SKILL.md +46 -106
  83. package/skills/security-rules/SKILL.md +34 -56
  84. package/skills/testing-rules/SKILL.md +28 -52
  85. package/skills/ultra-change/SKILL.md +68 -114
  86. package/skills/ultra-deliver/SKILL.md +58 -86
  87. package/skills/ultra-dev/SKILL.md +46 -105
  88. package/skills/ultra-doctor/SKILL.md +45 -72
  89. package/skills/ultra-init/SKILL.md +62 -147
  90. package/skills/ultra-init/references/brownfield-adoption.md +65 -0
  91. package/skills/ultra-plan/SKILL.md +56 -89
  92. package/skills/ultra-research/SKILL.md +64 -152
  93. package/skills/ultra-research/references/architecture.md +38 -0
  94. package/skills/ultra-research/references/problem-product.md +39 -0
  95. package/skills/ultra-research/references/quality-delivery.md +34 -0
  96. package/skills/ultra-research/references/synthesis.md +38 -0
  97. package/skills/ultra-research/references/users-scope.md +40 -0
  98. package/skills/ultra-review/SKILL.md +54 -98
  99. package/skills/ultra-review/references/unified-schema.md +76 -101
  100. package/skills/ultra-review/scripts/review_wait.py +242 -77
  101. package/skills/ultra-status/SKILL.md +41 -56
  102. package/skills/ultra-test/SKILL.md +37 -72
  103. package/skills/ultra-think/SKILL.md +43 -102
  104. package/skills/ultra-verify/SKILL.md +44 -84
  105. package/skills/ultra-verify/evals/evals.json +24 -27
  106. package/spec/cli-protocol.md +4 -0
  107. package/spec/mcp-tools.yaml +223 -4
  108. package/spec/schemas/command-manifest.schema.json +7 -25
  109. package/spec/schemas/mcp-tools.schema.json +1 -1
  110. package/spec/schemas/skill-manifest.schema.json +9 -79
  111. package/spec/schemas/state-db.sql +33 -2
  112. package/ultra-tools/commands/status.cjs +20 -2
  113. package/ultra-tools/commands/task.cjs +4 -1
  114. package/ultra-tools/status-command.test.cjs +20 -0
  115. package/ultra-tools/system-command.test.cjs +5 -0
  116. package/skills/code-review-expert/references/code-quality-checklist.md +0 -112
  117. package/skills/code-review-expert/references/integration-checklist.md +0 -46
  118. package/skills/code-review-expert/references/security-checklist.md +0 -100
  119. package/skills/code-review-expert/references/solid-checklist.md +0 -65
  120. package/skills/ultra-research/steps/step-00-problem-validation.md +0 -154
  121. package/skills/ultra-research/steps/step-01-opportunity-discovery.md +0 -172
  122. package/skills/ultra-research/steps/step-02-market-assessment.md +0 -195
  123. package/skills/ultra-research/steps/step-03-competitive-landscape.md +0 -197
  124. package/skills/ultra-research/steps/step-04-product-strategy.md +0 -196
  125. package/skills/ultra-research/steps/step-05-assumptions-validation.md +0 -234
  126. package/skills/ultra-research/steps/step-10-user-personas.md +0 -169
  127. package/skills/ultra-research/steps/step-11-user-scenarios.md +0 -181
  128. package/skills/ultra-research/steps/step-20-user-stories.md +0 -156
  129. package/skills/ultra-research/steps/step-21-features-scope.md +0 -157
  130. package/skills/ultra-research/steps/step-22-success-metrics.md +0 -195
  131. package/skills/ultra-research/steps/step-30-architecture-context.md +0 -154
  132. package/skills/ultra-research/steps/step-31-solution-strategy.md +0 -172
  133. package/skills/ultra-research/steps/step-32-building-blocks.md +0 -188
  134. package/skills/ultra-research/steps/step-40-deployment.md +0 -123
  135. package/skills/ultra-research/steps/step-41-quality-risks.md +0 -135
  136. package/skills/ultra-research/steps/step-99-synthesis.md +0 -188
  137. package/skills/ultra-review/scripts/review_verdict_update.py +0 -99
@@ -8,12 +8,10 @@ const { spawnSync } = require('node:child_process');
8
8
  const ops = require('./state-ops.cjs');
9
9
  const contextSpine = require('./context-spine.cjs');
10
10
  const specLearning = require('./spec-learning.cjs');
11
+ const providerRefs = require('./provider-refs.cjs');
12
+ const baselines = require('./baseline-workflow.cjs');
11
13
 
12
14
  const CHANGE_ID = /^[a-zA-Z0-9_-]+$/;
13
- const PROVIDER_KINDS = new Set(['memory', 'code_graph']);
14
- const PROVIDER_FIELDS = new Set([
15
- 'provider', 'project', 'reference', 'revision', 'indexed_head', 'status',
16
- ]);
17
15
  const CHANGE_PATCH_FIELDS = new Set(['title', 'intent', 'status', 'docs_impact', 'provider_refs']);
18
16
  const CHANGE_TRANSITIONS = Object.freeze({
19
17
  active: new Set(['active', 'blocked', 'cancelled']),
@@ -83,29 +81,7 @@ function normalizeDocsImpact(value) {
83
81
  }
84
82
 
85
83
  function normalizeProviderRefs(value) {
86
- if (value == null) return {};
87
- if (typeof value !== 'object' || Array.isArray(value)) {
88
- throw new ChangeWorkflowError('VALIDATION_ERROR', 'provider_refs must be an object');
89
- }
90
- const out = {};
91
- for (const [kind, ref] of Object.entries(value)) {
92
- if (!PROVIDER_KINDS.has(kind) || !ref || typeof ref !== 'object' || Array.isArray(ref)) {
93
- throw new ChangeWorkflowError('VALIDATION_ERROR', `invalid external provider reference: ${kind}`);
94
- }
95
- for (const key of Object.keys(ref)) {
96
- if (!PROVIDER_FIELDS.has(key)) {
97
- throw new ChangeWorkflowError(
98
- 'PROVIDER_CONTENT_FORBIDDEN',
99
- `provider_refs.${kind}.${key} is not metadata; provider content remains external`,
100
- );
101
- }
102
- }
103
- if (typeof ref.provider !== 'string' || !ref.provider.trim()) {
104
- throw new ChangeWorkflowError('VALIDATION_ERROR', `provider_refs.${kind}.provider required`);
105
- }
106
- out[kind] = { ...ref };
107
- }
108
- return out;
84
+ return providerRefs.normalizeProviderRefs(value, ChangeWorkflowError);
109
85
  }
110
86
 
111
87
  function gitHead(rootDir) {
@@ -254,6 +230,7 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
254
230
  readiness: spine.readiness,
255
231
  context: spine.context,
256
232
  execution_contract: spine.execution_contract,
233
+ baseline: spine.baseline,
257
234
  resume: spine.resume,
258
235
  providers,
259
236
  provider_boundary: 'metadata references only; memory and code graph content remain external',
@@ -504,6 +481,13 @@ function evidenceBlockers(change, evidence, tasks) {
504
481
  return blockers;
505
482
  }
506
483
 
484
+ function baselineConvergenceBlockers(db) {
485
+ const baseline = baselines.readBaseline(db);
486
+ if (!baseline) return ['BASELINE_MISSING'];
487
+ if (baseline.status !== 'ready') return [`BASELINE_NOT_READY:${baseline.status}`];
488
+ return [];
489
+ }
490
+
507
491
  function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
508
492
  const change = readChange(db, input.id);
509
493
  if (!change) throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${input.id} not found`);
@@ -512,6 +496,7 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
512
496
  }
513
497
  const tasks = db.prepare('SELECT id, type, status, stale FROM tasks WHERE change_id = ? ORDER BY id').all(change.id);
514
498
  const blockers = new Set(evidenceBlockers(change, input.evidence, tasks));
499
+ for (const blocker of baselineConvergenceBlockers(db)) blockers.add(blocker);
515
500
  if (tasks.length === 0) blockers.add('NO_TASKS');
516
501
  if (tasks.some((task) => !['completed', 'expanded'].includes(task.status))) blockers.add('TASKS_INCOMPLETE');
517
502
  if (tasks.some((task) => Boolean(task.stale))) blockers.add('TASK_CONTEXT_STALE');
@@ -671,6 +656,18 @@ function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
671
656
  });
672
657
  db.prepare("UPDATE artifacts SET status = 'archived', updated_at = ? WHERE change_id = ?")
673
658
  .run(nowIso(), change.id);
659
+ baselines.reconcileBaseline(db, {
660
+ baseline_updates: updates,
661
+ change_id: change.id,
662
+ }, { rootDir });
663
+ const baselineHealth = baselines.inspectBaseline(db, { rootDir });
664
+ if (baselineHealth.blockers.length > 0) {
665
+ throw new ChangeWorkflowError(
666
+ 'BASELINE_RECONCILIATION_INCOMPLETE',
667
+ `baseline reconciliation remains incomplete: ${baselineHealth.blockers.join(', ')}`,
668
+ { blockers: baselineHealth.blockers },
669
+ );
670
+ }
674
671
  ops.appendEventInTx(db, {
675
672
  type: 'change_archived', change_id: change.id,
676
673
  payload: { archive_path: relative, baseline_updates: updates, no_baseline_change_reason: noChangeReason || null },
@@ -10,6 +10,7 @@ const { execFileSync } = require('node:child_process');
10
10
  const { initStateDb, closeStateDb } = require('./state-db.cjs');
11
11
  const ops = require('./state-ops.cjs');
12
12
  const changes = require('./change-workflow.cjs');
13
+ const baselines = require('./baseline-workflow.cjs');
13
14
 
14
15
  function fixture() {
15
16
  const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-change-'));
@@ -20,6 +21,11 @@ function fixture() {
20
21
  execFileSync('git', ['add', 'README.md'], { cwd: rootDir });
21
22
  execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: rootDir });
22
23
  const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
24
+ db.prepare(
25
+ `INSERT INTO baselines
26
+ (id, project_name, mode, status, approved_by, approval_note, converged_at)
27
+ VALUES ('test-baseline', 'fixture', 'migrated', 'ready', 'test', 'legacy fixture', ?)`,
28
+ ).run(new Date().toISOString());
23
29
  return { rootDir, db };
24
30
  }
25
31
 
@@ -179,14 +185,82 @@ test('convergeChange blocks incomplete work and marks a fully evidenced standard
179
185
  }
180
186
  });
181
187
 
188
+ test('convergeChange requires an approved baseline after active work has remained executable', () => {
189
+ const fx = fixture();
190
+ try {
191
+ changes.createChange(fx.db, {
192
+ id: 'chg-baseline-gate', title: 'Keep runtime work moving', kind: 'quick',
193
+ intent: 'Defer baseline closure to convergence without losing implementation progress.',
194
+ docs_impact: { status: 'none', files: [], rationale: 'Internal workflow behavior only.' },
195
+ }, { rootDir: fx.rootDir });
196
+ ops.createTask(fx.db, {
197
+ id: 'baseline-gate-task', title: 'Exercise convergence boundary', type: 'feature', priority: 'P0',
198
+ change_id: 'chg-baseline-gate',
199
+ });
200
+ ops.updateTaskStatus(fx.db, 'baseline-gate-task', 'in_progress');
201
+ ops.updateTaskStatus(fx.db, 'baseline-gate-task', 'completed');
202
+ changes.compileContext(fx.db, {
203
+ id: 'chg-baseline-gate', ...executionContext('baseline-gate-task'),
204
+ }, { rootDir: fx.rootDir });
205
+ fx.db.prepare('DELETE FROM baselines').run();
206
+
207
+ const result = changes.convergeChange(fx.db, {
208
+ id: 'chg-baseline-gate',
209
+ evidence: [
210
+ { category: 'diff', status: 'pass', evidence: 'diff reviewed' },
211
+ { category: 'tests', status: 'pass', evidence: 'workflow regression passed', seam: 'convergence gate' },
212
+ { category: 'spec', status: 'pass', evidence: 'change remains aligned' },
213
+ ],
214
+ }, { rootDir: fx.rootDir });
215
+
216
+ assert.equal(result.ready, false);
217
+ assert.ok(result.blockers.includes('BASELINE_MISSING'));
218
+ } finally {
219
+ cleanup(fx);
220
+ }
221
+ });
222
+
182
223
  test('archiveChange moves a ready change into immutable history and records baseline reconciliation', () => {
183
224
  const fx = fixture();
184
225
  try {
226
+ fs.mkdirSync(path.join(fx.rootDir, '.ultra', 'specs'), { recursive: true });
227
+ fs.writeFileSync(path.join(fx.rootDir, '.ultra', 'specs', 'product.md'), '# Product\n\nCurrent behavior.\n');
228
+ fs.writeFileSync(path.join(fx.rootDir, '.ultra', 'specs', 'architecture.md'), '# Architecture\n\nCurrent boundary.\n');
229
+ const fileDigest = (file) => require('node:crypto').createHash('sha256')
230
+ .update(fs.readFileSync(file)).digest('hex');
231
+ fx.db.prepare(
232
+ `UPDATE baselines SET mode = 'brownfield', repository_revision = ?, spec_refs_json = ?
233
+ WHERE id = 'test-baseline'`,
234
+ ).run(
235
+ execFileSync('git', ['rev-parse', 'HEAD'], { cwd: fx.rootDir, encoding: 'utf8' }).trim(),
236
+ JSON.stringify([
237
+ {
238
+ kind: 'product', path: '.ultra/specs/product.md',
239
+ digest: fileDigest(path.join(fx.rootDir, '.ultra', 'specs', 'product.md')),
240
+ },
241
+ {
242
+ kind: 'architecture', path: '.ultra/specs/architecture.md',
243
+ digest: fileDigest(path.join(fx.rootDir, '.ultra', 'specs', 'architecture.md')),
244
+ },
245
+ ]),
246
+ );
185
247
  changes.createChange(fx.db, {
186
248
  id: 'chg-archive', title: 'Archive verified change', kind: 'quick',
187
249
  intent: 'Preserve the completed change as project history.',
188
250
  docs_impact: { status: 'none', files: [], rationale: 'Internal test-only behavior.' },
189
251
  }, { rootDir: fx.rootDir });
252
+ fs.appendFileSync(
253
+ path.join(fx.rootDir, '.ultra', 'specs', 'product.md'),
254
+ '\nBehavior learned and approved by this change.\n',
255
+ );
256
+ execFileSync('git', ['add', '.ultra/specs/product.md'], { cwd: fx.rootDir });
257
+ execFileSync('git', ['commit', '-q', '-m', 'update baseline behavior'], { cwd: fx.rootDir });
258
+ const changedHead = execFileSync(
259
+ 'git', ['rev-parse', 'HEAD'], { cwd: fx.rootDir, encoding: 'utf8' },
260
+ ).trim();
261
+ const drift = baselines.inspectBaseline(fx.db, { rootDir: fx.rootDir });
262
+ assert.ok(drift.blockers.includes('BASELINE_HEAD_STALE'));
263
+ assert.ok(drift.blockers.includes('BASELINE_SPEC_STALE:.ultra/specs/product.md'));
190
264
  ops.createTask(fx.db, {
191
265
  id: 'archive-task', title: 'Complete archive path', type: 'bugfix', priority: 'P1',
192
266
  change_id: 'chg-archive',
@@ -214,7 +288,7 @@ test('archiveChange moves a ready change into immutable history and records base
214
288
 
215
289
  const archived = changes.archiveChange(fx.db, {
216
290
  id: 'chg-archive', summary: 'Verified quick fix archived.',
217
- no_baseline_change_reason: 'No canonical spec changed.',
291
+ baseline_updates: ['.ultra/specs/product.md'],
218
292
  }, { rootDir: fx.rootDir });
219
293
  assert.equal(archived.change.status, 'archived');
220
294
  assert.ok(fs.existsSync(archived.archive_path));
@@ -228,6 +302,84 @@ test('archiveChange moves a ready change into immutable history and records base
228
302
  assert.doesNotMatch(artifact.path, /changes\/active/);
229
303
  assert.ok(fs.existsSync(path.join(fx.rootDir, artifact.path)), artifact.path);
230
304
  }
305
+ const reconciliation = fx.db.prepare(
306
+ "SELECT type, payload_json FROM events WHERE type = 'baseline_reconciled' ORDER BY id DESC LIMIT 1",
307
+ ).get();
308
+ assert.equal(reconciliation.type, 'baseline_reconciled');
309
+ assert.deepEqual(JSON.parse(reconciliation.payload_json).baseline_updates, ['.ultra/specs/product.md']);
310
+ const reconciled = baselines.inspectBaseline(fx.db, { rootDir: fx.rootDir });
311
+ assert.equal(reconciled.status, 'pass');
312
+ assert.equal(reconciled.baseline.repository_revision, changedHead);
313
+ } finally {
314
+ cleanup(fx);
315
+ }
316
+ });
317
+
318
+ test('archiveChange rolls back when declared reconciliation leaves a tracked specification stale', () => {
319
+ const fx = fixture();
320
+ try {
321
+ const specPath = path.join(fx.rootDir, '.ultra', 'specs', 'product.md');
322
+ fs.mkdirSync(path.dirname(specPath), { recursive: true });
323
+ fs.writeFileSync(specPath, '# Product\n\nOriginal behavior.\n');
324
+ const originalHead = execFileSync(
325
+ 'git', ['rev-parse', 'HEAD'], { cwd: fx.rootDir, encoding: 'utf8' },
326
+ ).trim();
327
+ const originalDigest = require('node:crypto').createHash('sha256')
328
+ .update(fs.readFileSync(specPath)).digest('hex');
329
+ fx.db.prepare(
330
+ `UPDATE baselines SET mode = 'brownfield', repository_revision = ?, spec_refs_json = ?
331
+ WHERE id = 'test-baseline'`,
332
+ ).run(originalHead, JSON.stringify([
333
+ { kind: 'product', path: '.ultra/specs/product.md', digest: originalDigest },
334
+ ]));
335
+ changes.createChange(fx.db, {
336
+ id: 'chg-reconcile-rollback', title: 'Reject incomplete reconciliation', kind: 'quick',
337
+ intent: 'Keep the active change recoverable until every tracked specification is reconciled.',
338
+ docs_impact: { status: 'none', files: [], rationale: 'Workflow integrity only.' },
339
+ }, { rootDir: fx.rootDir });
340
+ fs.appendFileSync(specPath, '\nChanged behavior that must be declared.\n');
341
+ execFileSync('git', ['add', '.ultra/specs/product.md'], { cwd: fx.rootDir });
342
+ execFileSync('git', ['commit', '-q', '-m', 'change tracked product spec'], { cwd: fx.rootDir });
343
+ ops.createTask(fx.db, {
344
+ id: 'reconcile-rollback-task', title: 'Exercise archive rollback', type: 'feature', priority: 'P0',
345
+ change_id: 'chg-reconcile-rollback',
346
+ });
347
+ ops.updateTaskStatus(fx.db, 'reconcile-rollback-task', 'in_progress');
348
+ ops.updateTaskStatus(fx.db, 'reconcile-rollback-task', 'completed');
349
+ changes.compileContext(fx.db, {
350
+ id: 'chg-reconcile-rollback', ...executionContext('reconcile-rollback-task'),
351
+ }, { rootDir: fx.rootDir });
352
+ const converged = changes.convergeChange(fx.db, {
353
+ id: 'chg-reconcile-rollback',
354
+ evidence: [
355
+ { category: 'diff', status: 'pass', evidence: 'archive diff reviewed' },
356
+ {
357
+ category: 'tests', status: 'pass', evidence: 'archive rollback regression passed',
358
+ seam: 'archive reconciliation boundary',
359
+ },
360
+ { category: 'spec', status: 'pass', evidence: 'tracked specification change identified' },
361
+ ],
362
+ }, { rootDir: fx.rootDir });
363
+ assert.equal(converged.ready, true);
364
+
365
+ assert.throws(
366
+ () => changes.archiveChange(fx.db, {
367
+ id: 'chg-reconcile-rollback', summary: 'Attempt an incomplete archive.',
368
+ no_baseline_change_reason: 'No baseline update declared.',
369
+ }, { rootDir: fx.rootDir }),
370
+ (error) => error.code === 'BASELINE_RECONCILIATION_INCOMPLETE'
371
+ && error.details.blockers.includes('BASELINE_SPEC_STALE:.ultra/specs/product.md'),
372
+ );
373
+
374
+ const active = changes.readChange(fx.db, 'chg-reconcile-rollback');
375
+ assert.equal(active.status, 'ready');
376
+ assert.ok(fs.existsSync(path.join(fx.rootDir, active.artifact_root)));
377
+ assert.equal(fs.existsSync(path.join(fx.rootDir, active.artifact_root, 'archive-summary.md')), false);
378
+ assert.equal(baselines.readBaseline(fx.db).repository_revision, originalHead);
379
+ const reconciliationEvents = fx.db.prepare(
380
+ "SELECT COUNT(*) AS count FROM events WHERE type = 'baseline_reconciled' AND change_id = ?",
381
+ ).get('chg-reconcile-rollback');
382
+ assert.equal(reconciliationEvents.count, 0);
231
383
  } finally {
232
384
  cleanup(fx);
233
385
  }
@@ -447,12 +599,12 @@ test('compileContext v2 persists role-scoped context, budget, execution contract
447
599
  }
448
600
  });
449
601
 
450
- test('compileContext blocks a required missing ref or an over-budget role packet', () => {
602
+ test('compileContext blocks a required missing ref', () => {
451
603
  const fx = fixture();
452
604
  try {
453
605
  changes.createChange(fx.db, {
454
606
  id: 'chg-context-budget', title: 'Bound context packet', kind: 'quick',
455
- intent: 'Fail closed when required context is missing or too large.',
607
+ intent: 'Fail closed when required context is missing while reporting size pressure.',
456
608
  }, { rootDir: fx.rootDir });
457
609
  ops.createTask(fx.db, {
458
610
  id: 'budget-task', title: 'Compile bounded packet', type: 'bugfix', priority: 'P0',
@@ -466,14 +618,58 @@ test('compileContext blocks a required missing ref or an over-budget role packet
466
618
  { ref: 'missing-contract.md', kind: 'spec', reason: 'Required contract', required: true },
467
619
  { ref: 'README.md', kind: 'source', reason: 'Current implementation', required: true },
468
620
  ],
469
- budget: { max_tokens: 1, max_files: 1 },
621
+ budget: { max_tokens: 2_000, max_files: 4 },
470
622
  }),
471
623
  }, { rootDir: fx.rootDir });
472
624
 
473
625
  assert.equal(compiled.manifest.readiness.status, 'blocked');
474
626
  assert.ok(compiled.manifest.readiness.blockers.includes('CONTEXT_REQUIRED_REF_MISSING:missing-contract.md'));
475
- assert.ok(compiled.manifest.readiness.blockers.includes('CONTEXT_FILE_BUDGET_EXCEEDED'));
476
- assert.ok(compiled.manifest.readiness.blockers.includes('CONTEXT_TOKEN_BUDGET_EXCEEDED'));
627
+ } finally {
628
+ cleanup(fx);
629
+ }
630
+ });
631
+
632
+ test('an incident can proceed with four necessary large files while context budgets remain advisory', () => {
633
+ const fx = fixture();
634
+ try {
635
+ const refs = [];
636
+ for (let index = 1; index <= 4; index += 1) {
637
+ const ref = `incident-runtime-${index}.cjs`;
638
+ fs.writeFileSync(path.join(fx.rootDir, ref), 'x'.repeat(8_000));
639
+ refs.push({
640
+ ref, kind: 'source', reason: `Required incident runtime boundary ${index}`, required: true,
641
+ });
642
+ }
643
+ changes.createChange(fx.db, {
644
+ id: 'chg-incident-context', title: 'Recover IM runtime', kind: 'incident',
645
+ intent: 'Keep an urgent root-cause fix executable when its required files exceed guidance.',
646
+ }, { rootDir: fx.rootDir });
647
+ ops.createTask(fx.db, {
648
+ id: 'incident-context-task', title: 'Repair IM terminal transition', type: 'bugfix', priority: 'P0',
649
+ change_id: 'chg-incident-context',
650
+ });
651
+
652
+ const compiled = changes.compileContext(fx.db, {
653
+ id: 'chg-incident-context',
654
+ ...executionContext('incident-context-task', {
655
+ context_refs: refs,
656
+ budget: { max_tokens: 100, max_files: 2 },
657
+ execution_contract: {
658
+ slice_kind: 'tracer_bullet',
659
+ public_seam: 'IM terminal transition',
660
+ verification_command: 'node --test im-terminal.test.cjs',
661
+ context_budget_percent: 80,
662
+ },
663
+ }),
664
+ }, { rootDir: fx.rootDir });
665
+
666
+ assert.equal(compiled.manifest.readiness.status, 'ready');
667
+ assert.deepEqual(compiled.manifest.readiness.blockers, []);
668
+ assert.ok(compiled.manifest.readiness.warnings.includes('CONTEXT_FILE_BUDGET_EXCEEDED'));
669
+ assert.ok(compiled.manifest.readiness.warnings.includes('CONTEXT_TOKEN_BUDGET_EXCEEDED'));
670
+ assert.ok(compiled.manifest.readiness.warnings.includes('EXECUTION_CONTEXT_BUDGET_ADVISORY'));
671
+ assert.equal(compiled.manifest.context.file_count, 4);
672
+ assert.ok(compiled.manifest.context.token_estimate > 100);
477
673
  } finally {
478
674
  cleanup(fx);
479
675
  }
@@ -4,6 +4,7 @@ const crypto = require('node:crypto');
4
4
  const fs = require('node:fs');
5
5
  const path = require('node:path');
6
6
  const { spawnSync } = require('node:child_process');
7
+ const baselines = require('./baseline-workflow.cjs');
7
8
 
8
9
  const ROLES = new Set(['plan', 'implement', 'check', 'review']);
9
10
  const GATES = new Set([
@@ -12,6 +13,12 @@ const GATES = new Set([
12
13
  const CONTEXT_KINDS = new Set(['spec', 'source', 'test', 'docs', 'external']);
13
14
  const SLICE_KINDS = new Set(['tracer_bullet', 'expand_contract', 'integration_checkpoint']);
14
15
  const DEFAULT_BUDGET = Object.freeze({ max_tokens: 12_000, max_files: 12 });
16
+ const ADVISORY_CONTEXT_CODES = new Set([
17
+ 'CONTEXT_FILE_BUDGET_EXCEEDED',
18
+ 'CONTEXT_TOKEN_BUDGET_EXCEEDED',
19
+ 'EXECUTION_CONTEXT_BUDGET_EXCEEDED',
20
+ 'EXECUTION_CONTEXT_BUDGET_ADVISORY',
21
+ ]);
15
22
 
16
23
  class ContextSpineError extends Error {
17
24
  constructor(code, message) {
@@ -149,8 +156,13 @@ function defaultGate(role) {
149
156
  }[role];
150
157
  }
151
158
 
152
- function deriveNextAction({ change, tasks, task, role, readiness, explicit }) {
153
- if (readiness === 'blocked') return 'Resolve the context readiness blockers, then recompile change.context.';
159
+ function deriveNextAction({ change, tasks, task, role, readiness, blockers = [], explicit }) {
160
+ if (readiness === 'blocked') {
161
+ if (blockers.some((blocker) => blocker.startsWith('BASELINE_'))) {
162
+ return 'Complete or refresh the Ultra project baseline, then recompile change.context.';
163
+ }
164
+ return 'Resolve the context readiness blockers, then recompile change.context.';
165
+ }
154
166
  if (explicit && String(explicit).trim()) return String(explicit).trim();
155
167
  if (change.status === 'ready') return `Run ultra-deliver for change ${change.id}.`;
156
168
  if (change.status === 'blocked') return `Resolve the blockers for change ${change.id}, then recompile context.`;
@@ -199,12 +211,19 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
199
211
  input, inherited.execution_contract, { taskId: input.task_id },
200
212
  );
201
213
  const blockers = [];
214
+ const warnings = [];
215
+ const baselineHealth = baselines.inspectBaseline(db, { rootDir });
216
+ // An active change must remain executable even when adoption is incomplete or
217
+ // the repository has moved. Approved adoption is required at convergence and
218
+ // full revision/spec health is required at archive; here both are recovery
219
+ // guidance, not a refusal.
220
+ warnings.push(...baselineHealth.blockers, ...baselineHealth.warnings);
202
221
  for (const item of items) {
203
222
  if (item.required && item.status === 'missing') blockers.push(`CONTEXT_REQUIRED_REF_MISSING:${item.ref}`);
204
223
  if (item.required && item.status === 'stale') blockers.push(`CONTEXT_REQUIRED_REF_STALE:${item.ref}`);
205
224
  }
206
- if (fileCount > budget.max_files) blockers.push('CONTEXT_FILE_BUDGET_EXCEEDED');
207
- if (tokenEstimate > budget.max_tokens) blockers.push('CONTEXT_TOKEN_BUDGET_EXCEEDED');
225
+ if (fileCount > budget.max_files) warnings.push('CONTEXT_FILE_BUDGET_EXCEEDED');
226
+ if (tokenEstimate > budget.max_tokens) warnings.push('CONTEXT_TOKEN_BUDGET_EXCEEDED');
208
227
  if (input.task_id) {
209
228
  if (!executionContract) blockers.push('EXECUTION_CONTRACT_MISSING');
210
229
  else {
@@ -212,21 +231,29 @@ function compileRoleContext(db, { input, change, tasks, rootDir }) {
212
231
  if (!executionContract.verification_command) blockers.push('EXECUTION_VERIFICATION_COMMAND_MISSING');
213
232
  if (executionContract.slice_kind !== 'expand_contract'
214
233
  && executionContract.context_budget_percent > 40) {
215
- blockers.push('EXECUTION_CONTEXT_BUDGET_EXCEEDED');
234
+ warnings.push('EXECUTION_CONTEXT_BUDGET_ADVISORY');
216
235
  }
217
236
  }
218
237
  }
219
238
  const readiness = blockers.length === 0 ? 'ready' : 'blocked';
220
239
  const nextAction = deriveNextAction({
221
- change, tasks, task, role, readiness, explicit: input.next_action,
240
+ change, tasks, task, role, readiness, blockers, explicit: input.next_action,
222
241
  });
223
242
  return {
224
243
  role,
225
244
  gate,
226
245
  next_action: nextAction,
227
- readiness: { status: readiness, blockers },
246
+ readiness: { status: readiness, blockers, warnings: [...new Set(warnings)] },
228
247
  context: { items, budget, token_estimate: tokenEstimate, file_count: fileCount },
229
248
  execution_contract: executionContract,
249
+ baseline: baselineHealth.baseline ? {
250
+ id: baselineHealth.baseline.id,
251
+ mode: baselineHealth.baseline.mode,
252
+ status: baselineHealth.baseline.status,
253
+ repository_revision: baselineHealth.baseline.repository_revision,
254
+ health: baselineHealth.status,
255
+ warnings: [...new Set([...baselineHealth.blockers, ...baselineHealth.warnings])],
256
+ } : null,
230
257
  selected_task: task,
231
258
  resume: {
232
259
  change_id: change.id,
@@ -250,6 +277,7 @@ function recommendedWorkflow(change, task, tasks, readiness) {
250
277
  }
251
278
 
252
279
  function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
280
+ const baselineHealth = baselines.inspectBaseline(db, { rootDir });
253
281
  const change = id
254
282
  ? db.prepare('SELECT * FROM changes WHERE id = ?').get(id)
255
283
  : db.prepare(
@@ -259,10 +287,32 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
259
287
  throw new ContextSpineError('CHANGE_NOT_FOUND', `change ${id} not found`);
260
288
  }
261
289
  if (!change) {
290
+ const baseline = baselineHealth.baseline;
291
+ if (baselineHealth.status !== 'pass') {
292
+ const nextAction = baseline
293
+ ? `Complete ${baseline.mode} baseline adoption ${baseline.id} through ultra-init.`
294
+ : 'Initialize the project and start baseline adoption with ultra-init.';
295
+ return {
296
+ change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'blocked',
297
+ blockers: baselineHealth.blockers, warnings: baselineHealth.warnings, next_action: nextAction,
298
+ recommended_workflow: 'ultra-init', context_manifest_path: null,
299
+ context_manifest_hash: null, git_head: currentHead(rootDir),
300
+ baseline: baseline ? {
301
+ id: baseline.id, mode: baseline.mode, status: baseline.status,
302
+ repository_revision: baseline.repository_revision,
303
+ } : null,
304
+ };
305
+ }
262
306
  return {
263
307
  change_id: null, task_id: null, role: 'plan', gate: 'alignment', readiness: 'ready',
264
308
  next_action: 'Start daily work with ultra-change.', recommended_workflow: 'ultra-change',
265
309
  context_manifest_path: null, context_manifest_hash: null, git_head: currentHead(rootDir),
310
+ blockers: [], warnings: baselineHealth.warnings,
311
+ baseline: {
312
+ id: baselineHealth.baseline.id, mode: baselineHealth.baseline.mode,
313
+ status: baselineHealth.baseline.status,
314
+ repository_revision: baselineHealth.baseline.repository_revision,
315
+ },
266
316
  };
267
317
  }
268
318
  const tasks = db.prepare('SELECT * FROM tasks WHERE change_id = ? ORDER BY created_at ASC').all(change.id);
@@ -275,18 +325,26 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
275
325
  const task = tasks.find((row) => row.id === taskId) || null;
276
326
  const head = currentHead(rootDir);
277
327
  const snapshotMissing = !snapshot;
278
- const legacyContext = Boolean(snapshot && (!context.resume || !context.context || !context.readiness));
328
+ const legacyContext = Boolean(
329
+ snapshot && (!context.resume || !context.context || !context.readiness || !('baseline' in context)),
330
+ );
279
331
  const headStale = Boolean(snapshot?.git_head && head && snapshot.git_head !== head);
280
332
  const stateChanged = context.resume?.task_status !== undefined
281
333
  && context.resume.task_status !== (task?.status || null);
282
334
  const blockers = [];
335
+ const warnings = [...baselineHealth.blockers, ...baselineHealth.warnings];
283
336
  if (snapshotMissing) blockers.push('CONTEXT_NOT_COMPILED');
284
337
  else {
285
338
  if (legacyContext) blockers.push('CONTEXT_SNAPSHOT_UPGRADE_REQUIRED');
286
339
  if (headStale) blockers.push('CONTEXT_HEAD_STALE');
287
340
  if (stateChanged) blockers.push('CONTEXT_TASK_STATE_STALE');
288
341
  for (const blocker of parseJson(snapshot.blockers_json, [])) {
289
- if (!blockers.includes(blocker)) blockers.push(blocker);
342
+ if (ADVISORY_CONTEXT_CODES.has(blocker) || blocker.startsWith('BASELINE_')) {
343
+ if (!warnings.includes(blocker)) warnings.push(blocker);
344
+ } else if (!blockers.includes(blocker)) blockers.push(blocker);
345
+ }
346
+ for (const warning of context.readiness?.warnings || []) {
347
+ if (!warnings.includes(warning)) warnings.push(warning);
290
348
  }
291
349
  }
292
350
  const readiness = blockers.length > 0 ? 'blocked' : snapshot.readiness;
@@ -302,6 +360,7 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
302
360
  } else {
303
361
  nextAction = deriveNextAction({
304
362
  change, tasks, task, role: snapshot?.role || 'plan', readiness,
363
+ blockers,
305
364
  explicit: snapshot?.next_action,
306
365
  });
307
366
  }
@@ -315,11 +374,17 @@ function readBreadcrumb(db, { id } = {}, { rootDir = process.cwd() } = {}) {
315
374
  gate: snapshot?.gate || 'alignment',
316
375
  readiness,
317
376
  blockers,
377
+ warnings: [...new Set(warnings)],
318
378
  next_action: nextAction,
319
379
  recommended_workflow: recommendedWorkflow(change, task, tasks, readiness),
320
380
  context_manifest_path: snapshot?.manifest_path || null,
321
381
  context_manifest_hash: snapshot?.manifest_hash || null,
322
382
  git_head: head,
383
+ baseline: baselineHealth.baseline ? {
384
+ id: baselineHealth.baseline.id, mode: baselineHealth.baseline.mode,
385
+ status: baselineHealth.baseline.status,
386
+ repository_revision: baselineHealth.baseline.repository_revision,
387
+ } : null,
323
388
  };
324
389
  }
325
390
 
@@ -11,6 +11,21 @@ const { initStateDb } = require('./state-db.cjs');
11
11
  const { createChange } = require('./change-workflow.cjs');
12
12
  const { createTask } = require('./state-ops.cjs');
13
13
 
14
+ test('breadcrumb routes a project without a converged baseline back to adoption', () => {
15
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-spine-baseline-'));
16
+ const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
17
+ try {
18
+ const breadcrumb = readBreadcrumb(db, {}, { rootDir });
19
+ assert.equal(breadcrumb.readiness, 'blocked');
20
+ assert.ok(breadcrumb.blockers.includes('BASELINE_MISSING'));
21
+ assert.equal(breadcrumb.recommended_workflow, 'ultra-init');
22
+ assert.match(breadcrumb.next_action, /baseline|adoption/i);
23
+ } finally {
24
+ db.close();
25
+ fs.rmSync(rootDir, { recursive: true, force: true });
26
+ }
27
+ });
28
+
14
29
  test('deriveNextAction gives one state-specific action instead of a workflow dump', () => {
15
30
  const change = { id: 'context-spine', status: 'active' };
16
31
  const task = { id: 'task-1', status: 'in_progress' };
@@ -8,6 +8,7 @@ const ops = require('./state-ops.cjs');
8
8
  const runtime = require('./runtime-state.cjs');
9
9
  const projector = require('./projector.cjs');
10
10
  const recovery = require('../../orchestrator/recovery.cjs');
11
+ const baselines = require('./baseline-workflow.cjs');
11
12
 
12
13
  const SPEC_CONSUMER = 'spec-staleness';
13
14
 
@@ -48,6 +49,9 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
48
49
  WHERE status IN ('active', 'blocked', 'ready') AND artifact_root IS NOT NULL`,
49
50
  ).all().filter((row) => row.artifact_root && !fs.existsSync(path.resolve(rootDir, row.artifact_root))).length
50
51
  : 0;
52
+ const baseline = missing.length === 0
53
+ ? baselines.inspectBaseline(db, { rootDir })
54
+ : { status: 'fail', blockers: ['BASELINE_STATE_UNAVAILABLE'], warnings: [], baseline: null };
51
55
  const degraded = integrity !== 'ok' || missing.length > 0 || incidents.length > 0
52
56
  || pending.length > 0 || running.length > 0 || failed.length > 0 || orphanSessions > 0 || activeMissing > 0
53
57
  || eventCursor > projectedCursor;
@@ -63,6 +67,16 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
63
67
  },
64
68
  sessions: { status: orphanSessions === 0 ? 'pass' : 'fail', orphan: orphanSessions },
65
69
  change_artifacts: { status: activeMissing === 0 ? 'pass' : 'fail', missing: activeMissing },
70
+ baseline: {
71
+ status: baseline.status === 'pass' ? 'pass' : 'warning',
72
+ readiness: baseline.status,
73
+ mode: baseline.baseline?.mode || null,
74
+ baseline_status: baseline.baseline?.status || null,
75
+ id: baseline.baseline?.id || null,
76
+ repository_revision: baseline.baseline?.repository_revision || null,
77
+ blockers: baseline.blockers,
78
+ warnings: baseline.warnings,
79
+ },
66
80
  external_providers: {
67
81
  status: 'pass', ownership: 'external',
68
82
  note: 'Ultra stores provider metadata references only and never owns memory or code-graph content.',
@@ -10,10 +10,18 @@ const { initStateDb, closeStateDb } = require('./state-db.cjs');
10
10
  const ops = require('./state-ops.cjs');
11
11
  const runtime = require('./runtime-state.cjs');
12
12
  const doctor = require('./doctor.cjs');
13
+ const baselines = require('./baseline-workflow.cjs');
13
14
 
14
- function fixture() {
15
+ function fixture({ migratedBaseline = true } = {}) {
15
16
  const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-doctor-'));
16
17
  const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
18
+ if (migratedBaseline) {
19
+ db.prepare(
20
+ `INSERT INTO baselines
21
+ (id, project_name, mode, status, approved_by, approval_note, converged_at)
22
+ VALUES ('test-baseline', 'fixture', 'migrated', 'ready', 'test', 'legacy fixture', ?)`,
23
+ ).run(new Date().toISOString());
24
+ }
17
25
  return { rootDir, db };
18
26
  }
19
27
 
@@ -35,6 +43,22 @@ test('doctor reports structured health for an initialized Ultra project', async
35
43
  }
36
44
  });
37
45
 
46
+ test('doctor reports incomplete brownfield adoption as advisory rather than authority failure', async () => {
47
+ const fx = fixture({ migratedBaseline: false });
48
+ try {
49
+ baselines.startBaseline(fx.db, {
50
+ id: 'adoption', project_name: 'legacy', mode: 'brownfield', scope: ['.'],
51
+ }, { rootDir: fx.rootDir, emitEvent: false });
52
+ const report = await doctor.runDoctor(fx.db, { rootDir: fx.rootDir });
53
+ assert.equal(report.status, 'healthy');
54
+ assert.equal(report.checks.baseline.status, 'warning');
55
+ assert.equal(report.checks.baseline.mode, 'brownfield');
56
+ assert.ok(report.checks.baseline.blockers.includes('BASELINE_NOT_READY:adopting'));
57
+ } finally {
58
+ cleanup(fx);
59
+ }
60
+ });
61
+
38
62
  test('doctor repair is backup-first and drains projection/spec-event recovery work', async () => {
39
63
  const fx = fixture();
40
64
  try {