ultra-builder-pro-cli 0.11.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 (63) 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 +42 -1
  7. package/README.md +21 -9
  8. package/adapters/_shared/codex-assets.cjs +1 -1
  9. package/adapters/_shared/handbook.cjs +4 -3
  10. package/adapters/_shared/tests/handbook.test.cjs +4 -0
  11. package/adapters/opencode.js +8 -2
  12. package/adapters/tests/codex.test.cjs +2 -2
  13. package/adapters/tests/kimi-hook.test.cjs +1 -1
  14. package/adapters/tests/opencode.test.cjs +36 -1
  15. package/commands/ultra-init.md +3 -3
  16. package/docs/AGENT-CONTEXT.md +16 -8
  17. package/docs/ARCHITECTURE.md +32 -5
  18. package/docs/PLAN.zh-CN.md +7 -5
  19. package/docs/ROADMAP.md +4 -2
  20. package/docs/RUNTIME-COMPAT-MATRIX.md +8 -5
  21. package/docs/STATE-DB-ACCESS-POLICY.md +17 -11
  22. package/docs/USER-HANDBOOK-CONTRACT.md +8 -5
  23. package/hooks/README.md +4 -3
  24. package/hooks/adapters/codex.py +1 -1
  25. package/hooks/adapters/kimi.py +1 -1
  26. package/hooks/context_spine.py +120 -16
  27. package/hooks/health_check.py +11 -0
  28. package/hooks/pre_stop_check.py +6 -5
  29. package/mcp-server/lib/baseline-workflow.cjs +415 -0
  30. package/mcp-server/lib/baseline-workflow.test.cjs +225 -0
  31. package/mcp-server/lib/change-workflow.cjs +24 -27
  32. package/mcp-server/lib/change-workflow.test.cjs +202 -6
  33. package/mcp-server/lib/context-spine.cjs +74 -9
  34. package/mcp-server/lib/context-spine.test.cjs +15 -0
  35. package/mcp-server/lib/doctor.cjs +14 -0
  36. package/mcp-server/lib/doctor.test.cjs +25 -1
  37. package/mcp-server/lib/init-project.cjs +49 -14
  38. package/mcp-server/lib/provider-refs.cjs +42 -0
  39. package/mcp-server/lib/spec-learning.test.cjs +5 -0
  40. package/mcp-server/lib/state-db.cjs +63 -4
  41. package/mcp-server/lib/state-db.test.cjs +8 -3
  42. package/mcp-server/server.cjs +48 -3
  43. package/mcp-server/tests/init-project.test.cjs +54 -8
  44. package/mcp-server/tests/server-contract.test.cjs +99 -2
  45. package/mcp-server/tests/state-db.test.cjs +11 -2
  46. package/package.json +1 -1
  47. package/skills/ultra-change/SKILL.md +24 -13
  48. package/skills/ultra-deliver/SKILL.md +15 -5
  49. package/skills/ultra-dev/SKILL.md +4 -0
  50. package/skills/ultra-doctor/SKILL.md +8 -2
  51. package/skills/ultra-init/SKILL.md +46 -50
  52. package/skills/ultra-init/references/brownfield-adoption.md +65 -0
  53. package/skills/ultra-plan/SKILL.md +9 -2
  54. package/skills/ultra-research/SKILL.md +10 -5
  55. package/skills/ultra-status/SKILL.md +13 -6
  56. package/spec/cli-protocol.md +4 -0
  57. package/spec/mcp-tools.yaml +223 -4
  58. package/spec/schemas/mcp-tools.schema.json +1 -1
  59. package/spec/schemas/state-db.sql +33 -2
  60. package/ultra-tools/commands/status.cjs +20 -2
  61. package/ultra-tools/commands/task.cjs +4 -1
  62. package/ultra-tools/status-command.test.cjs +20 -0
  63. package/ultra-tools/system-command.test.cjs +5 -0
@@ -0,0 +1,225 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const crypto = require('node:crypto');
6
+ const fs = require('node:fs');
7
+ const os = require('node:os');
8
+ const path = require('node:path');
9
+ const { execFileSync } = require('node:child_process');
10
+
11
+ const { initStateDb, closeStateDb } = require('./state-db.cjs');
12
+ const baselines = require('./baseline-workflow.cjs');
13
+
14
+ function fixture() {
15
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-baseline-'));
16
+ execFileSync('git', ['init', '-q'], { cwd: rootDir });
17
+ execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: rootDir });
18
+ execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: rootDir });
19
+ fs.mkdirSync(path.join(rootDir, '.ultra', 'specs'), { recursive: true });
20
+ fs.mkdirSync(path.join(rootDir, 'src'), { recursive: true });
21
+ fs.writeFileSync(path.join(rootDir, '.ultra', 'specs', 'product.md'), '# Product\n\nObserved behavior.\n');
22
+ fs.writeFileSync(path.join(rootDir, '.ultra', 'specs', 'architecture.md'), '# Architecture\n\nCurrent boundary.\n');
23
+ fs.writeFileSync(path.join(rootDir, 'src', 'index.js'), 'module.exports = true;\n');
24
+ execFileSync('git', ['add', '.'], { cwd: rootDir });
25
+ execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: rootDir });
26
+ const revision = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: rootDir, encoding: 'utf8' }).trim();
27
+ const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
28
+ return { rootDir, db, revision };
29
+ }
30
+
31
+ function cleanup(fx) {
32
+ closeStateDb(fx.db);
33
+ fs.rmSync(fx.rootDir, { recursive: true, force: true });
34
+ }
35
+
36
+ function adoptionEvidence() {
37
+ return {
38
+ spec_refs: [
39
+ { kind: 'product', path: '.ultra/specs/product.md' },
40
+ { kind: 'architecture', path: '.ultra/specs/architecture.md' },
41
+ ],
42
+ evidence: [
43
+ { kind: 'source', ref: 'src/index.js', summary: 'Current public module entry point.' },
44
+ { kind: 'docs', ref: '.ultra/specs/product.md', summary: 'Observed behavior baseline.' },
45
+ ],
46
+ verification: [
47
+ { name: 'module smoke', command: 'node -e "require(\'./src\')"', status: 'pass', evidence: 'exit 0' },
48
+ ],
49
+ unknowns: [],
50
+ };
51
+ }
52
+
53
+ test('brownfield baseline converges only after current specs, source evidence, verification, and approval', () => {
54
+ const fx = fixture();
55
+ try {
56
+ const started = baselines.startBaseline(fx.db, {
57
+ id: 'project-baseline', project_name: 'fixture', project_type: 'cli', stack: 'node',
58
+ mode: 'brownfield', repository_revision: fx.revision, scope: ['.'],
59
+ }, { rootDir: fx.rootDir });
60
+ assert.equal(started.status, 'adopting');
61
+
62
+ const recorded = baselines.recordBaseline(fx.db, {
63
+ id: started.id, repository_revision: fx.revision, ...adoptionEvidence(),
64
+ }, { rootDir: fx.rootDir });
65
+ assert.match(recorded.spec_refs[0].digest, /^[0-9a-f]{64}$/);
66
+ assert.equal(recorded.evidence[0].summary, 'Current public module entry point.');
67
+
68
+ const converged = baselines.convergeBaseline(fx.db, {
69
+ id: started.id,
70
+ expected_revision: fx.revision,
71
+ approved_by: 'project-owner',
72
+ approval_note: 'The captured baseline matches the current checkout.',
73
+ accept_known_red: false,
74
+ }, { rootDir: fx.rootDir });
75
+ assert.deepEqual(converged.blockers, []);
76
+ assert.equal(converged.ready, true);
77
+ assert.equal(converged.baseline.status, 'ready');
78
+ assert.equal(converged.baseline.approved_by, 'project-owner');
79
+ } finally {
80
+ cleanup(fx);
81
+ }
82
+ });
83
+
84
+ test('baseline convergence records deterministic blockers for missing evidence and unaccepted known-red verification', () => {
85
+ const fx = fixture();
86
+ try {
87
+ baselines.startBaseline(fx.db, {
88
+ id: 'blocked-baseline', project_name: 'fixture', mode: 'brownfield',
89
+ repository_revision: fx.revision,
90
+ }, { rootDir: fx.rootDir });
91
+ baselines.recordBaseline(fx.db, {
92
+ id: 'blocked-baseline',
93
+ repository_revision: fx.revision,
94
+ spec_refs: [{ kind: 'product', path: '.ultra/specs/product.md' }],
95
+ evidence: [],
96
+ verification: [{
97
+ name: 'legacy suite', command: 'npm test', status: 'known_red',
98
+ evidence: 'Two failures predate adoption.', rationale: 'Tracked as existing debt.',
99
+ }],
100
+ unknowns: [{ summary: 'Production authorization behavior is not verified.', blocking: true }],
101
+ }, { rootDir: fx.rootDir });
102
+
103
+ const result = baselines.convergeBaseline(fx.db, {
104
+ id: 'blocked-baseline', expected_revision: fx.revision,
105
+ approved_by: 'project-owner', approval_note: 'Review attempted.', accept_known_red: false,
106
+ }, { rootDir: fx.rootDir });
107
+ assert.equal(result.ready, false);
108
+ assert.equal(result.status, 'blocked');
109
+ assert.ok(result.blockers.includes('BASELINE_SPEC_MISSING:architecture'));
110
+ assert.ok(result.blockers.includes('BASELINE_SOURCE_EVIDENCE_MISSING'));
111
+ assert.ok(result.blockers.includes('BASELINE_KNOWN_RED_NOT_ACCEPTED:legacy suite'));
112
+ assert.ok(result.blockers.includes('BASELINE_UNKNOWN_BLOCKING:Production authorization behavior is not verified.'));
113
+ } finally {
114
+ cleanup(fx);
115
+ }
116
+ });
117
+
118
+ test('baseline stores provider metadata only and detects specification drift after convergence', () => {
119
+ const fx = fixture();
120
+ try {
121
+ baselines.startBaseline(fx.db, {
122
+ id: 'provider-baseline', project_name: 'fixture', mode: 'brownfield',
123
+ repository_revision: fx.revision,
124
+ }, { rootDir: fx.rootDir });
125
+ assert.throws(
126
+ () => baselines.recordBaseline(fx.db, {
127
+ id: 'provider-baseline',
128
+ provider_refs: { memory: { provider: 'cloud-mem', content: 'captured prompt' } },
129
+ }, { rootDir: fx.rootDir }),
130
+ (error) => error.code === 'PROVIDER_CONTENT_FORBIDDEN',
131
+ );
132
+ baselines.recordBaseline(fx.db, {
133
+ id: 'provider-baseline', repository_revision: fx.revision,
134
+ provider_refs: { memory: { provider: 'cloud-mem', reference: 'cmem://fixture', status: 'fresh' } },
135
+ ...adoptionEvidence(),
136
+ }, { rootDir: fx.rootDir });
137
+ baselines.convergeBaseline(fx.db, {
138
+ id: 'provider-baseline', expected_revision: fx.revision,
139
+ approved_by: 'project-owner', approval_note: 'Approved current behavior.',
140
+ }, { rootDir: fx.rootDir });
141
+
142
+ fs.appendFileSync(path.join(fx.rootDir, '.ultra', 'specs', 'product.md'), '\nChanged outside convergence.\n');
143
+ const health = baselines.inspectBaseline(fx.db, { rootDir: fx.rootDir });
144
+ assert.equal(health.status, 'fail');
145
+ assert.ok(health.blockers.includes('BASELINE_SPEC_STALE:.ultra/specs/product.md'));
146
+ } finally {
147
+ cleanup(fx);
148
+ }
149
+ });
150
+
151
+ test('re-adoption requires explicit replacement and supersedes the prior ready baseline', () => {
152
+ const fx = fixture();
153
+ try {
154
+ const digest = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
155
+ fx.db.prepare(
156
+ `INSERT INTO baselines
157
+ (id, project_name, mode, status, repository_revision, spec_refs_json, evidence_json, verification_json,
158
+ approved_by, approval_note, converged_at)
159
+ VALUES (?, ?, 'brownfield', 'ready', ?, ?, ?, ?, ?, ?, ?)`,
160
+ ).run(
161
+ 'ready-baseline', 'fixture', fx.revision,
162
+ JSON.stringify([
163
+ { kind: 'product', path: '.ultra/specs/product.md', digest: digest(path.join(fx.rootDir, '.ultra/specs/product.md')) },
164
+ { kind: 'architecture', path: '.ultra/specs/architecture.md', digest: digest(path.join(fx.rootDir, '.ultra/specs/architecture.md')) },
165
+ ]),
166
+ JSON.stringify([{ kind: 'source', ref: 'src/index.js', summary: 'Current entry point.' }]),
167
+ JSON.stringify([{ name: 'smoke', command: 'node -e "require(\'./src\')"', status: 'pass', evidence: 'exit 0' }]),
168
+ 'owner', 'approved', new Date().toISOString(),
169
+ );
170
+
171
+ assert.throws(
172
+ () => baselines.startBaseline(fx.db, {
173
+ id: 'replacement', project_name: 'fixture', mode: 'brownfield', repository_revision: fx.revision,
174
+ }, { rootDir: fx.rootDir }),
175
+ (error) => error.code === 'BASELINE_EXISTS',
176
+ );
177
+ const replacement = baselines.startBaseline(fx.db, {
178
+ id: 'replacement', project_name: 'fixture', mode: 'brownfield',
179
+ repository_revision: fx.revision, replace_ready: true,
180
+ }, { rootDir: fx.rootDir });
181
+ assert.equal(replacement.status, 'adopting');
182
+ assert.equal(baselines.readBaseline(fx.db, 'ready-baseline').status, 'superseded');
183
+ const supersededHealth = baselines.inspectBaseline(fx.db, {
184
+ rootDir: fx.rootDir, id: 'ready-baseline',
185
+ });
186
+ assert.ok(supersededHealth.blockers.includes('BASELINE_NOT_READY:superseded'));
187
+ } finally {
188
+ cleanup(fx);
189
+ }
190
+ });
191
+
192
+ test('greenfield baseline derives a stable workspace revision when Git is not initialized', () => {
193
+ const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-greenfield-baseline-'));
194
+ fs.mkdirSync(path.join(rootDir, '.ultra', 'specs'), { recursive: true });
195
+ fs.writeFileSync(path.join(rootDir, '.ultra', 'specs', 'product.md'), '# Product\n\nAccepted intent.\n');
196
+ fs.writeFileSync(path.join(rootDir, '.ultra', 'specs', 'architecture.md'), '# Architecture\n\nAccepted constraints.\n');
197
+ const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
198
+ try {
199
+ baselines.startBaseline(db, {
200
+ id: 'greenfield', project_name: 'new-project', mode: 'greenfield', scope: ['.'],
201
+ }, { rootDir });
202
+ const recorded = baselines.recordBaseline(db, {
203
+ id: 'greenfield',
204
+ spec_refs: [
205
+ { kind: 'product', path: '.ultra/specs/product.md' },
206
+ { kind: 'architecture', path: '.ultra/specs/architecture.md' },
207
+ ],
208
+ evidence: [{ kind: 'docs', ref: '.ultra/specs/product.md', summary: 'Owner-approved product intent.' }],
209
+ verification: [{
210
+ name: 'spec review', command: 'review baseline specifications', status: 'pass',
211
+ evidence: 'Product and architecture contracts agree.',
212
+ }],
213
+ unknowns: [],
214
+ }, { rootDir });
215
+ assert.match(recorded.repository_revision, /^workspace:[0-9a-f]{64}$/);
216
+ const result = baselines.convergeBaseline(db, {
217
+ id: 'greenfield', expected_revision: recorded.repository_revision,
218
+ approved_by: 'owner', approval_note: 'Approved before planning.',
219
+ }, { rootDir });
220
+ assert.equal(result.ready, true);
221
+ } finally {
222
+ closeStateDb(db);
223
+ fs.rmSync(rootDir, { recursive: true, force: true });
224
+ }
225
+ });
@@ -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
  }