ultra-builder-pro-cli 0.12.1 → 0.13.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 (57) hide show
  1. package/.ultra-template/docs/research/README.md +21 -44
  2. package/.ultra-template/specs/discovery.md +10 -0
  3. package/CHANGELOG.md +53 -1
  4. package/README.md +25 -9
  5. package/adapters/_shared/handbook.cjs +2 -2
  6. package/adapters/tests/codex.test.cjs +8 -0
  7. package/commands/ultra-init.md +1 -1
  8. package/docs/AGENT-CONTEXT.md +4 -3
  9. package/docs/ARCHITECTURE.md +32 -16
  10. package/docs/PLAN.zh-CN.md +5 -4
  11. package/docs/ROADMAP.md +1 -1
  12. package/docs/STATE-DB-ACCESS-POLICY.md +3 -3
  13. package/docs/USER-HANDBOOK-CONTRACT.md +5 -4
  14. package/hooks/context_spine.py +37 -4
  15. package/mcp-server/lib/archive-journal.cjs +192 -0
  16. package/mcp-server/lib/archive-journal.test.cjs +35 -0
  17. package/mcp-server/lib/baseline-workflow.cjs +404 -38
  18. package/mcp-server/lib/baseline-workflow.test.cjs +252 -0
  19. package/mcp-server/lib/change-workflow.cjs +249 -75
  20. package/mcp-server/lib/change-workflow.test.cjs +235 -1
  21. package/mcp-server/lib/context-spine.test.cjs +5 -0
  22. package/mcp-server/lib/doctor.cjs +14 -1
  23. package/mcp-server/lib/doctor.test.cjs +42 -4
  24. package/mcp-server/lib/init-project.cjs +292 -9
  25. package/mcp-server/lib/spec-learning.test.cjs +1 -1
  26. package/mcp-server/lib/state-authority.cjs +9 -3
  27. package/mcp-server/lib/state-db.cjs +154 -32
  28. package/mcp-server/lib/state-db.test.cjs +136 -4
  29. package/mcp-server/lib/state-ops.cjs +68 -6
  30. package/mcp-server/lib/task-expander.cjs +7 -1
  31. package/mcp-server/lib/task-expander.test.cjs +71 -0
  32. package/mcp-server/server.cjs +14 -3
  33. package/mcp-server/tests/init-project.test.cjs +162 -1
  34. package/mcp-server/tests/migrate.test.cjs +33 -0
  35. package/mcp-server/tests/server-contract.test.cjs +384 -6
  36. package/mcp-server/tests/state-db.test.cjs +14 -2
  37. package/mcp-server/tests/state-ops.test.cjs +108 -0
  38. package/package.json +1 -1
  39. package/skills/ultra-change/SKILL.md +18 -10
  40. package/skills/ultra-deliver/SKILL.md +13 -9
  41. package/skills/ultra-dev/SKILL.md +6 -4
  42. package/skills/ultra-doctor/SKILL.md +22 -7
  43. package/skills/ultra-init/SKILL.md +104 -59
  44. package/skills/ultra-init/references/brownfield-adoption.md +65 -37
  45. package/skills/ultra-plan/SKILL.md +8 -5
  46. package/skills/ultra-research/SKILL.md +12 -5
  47. package/skills/ultra-status/SKILL.md +6 -4
  48. package/spec/cli-protocol.md +11 -1
  49. package/spec/mcp-tools.yaml +95 -9
  50. package/spec/schemas/state-db.sql +11 -0
  51. package/ultra-tools/cli.cjs +9 -5
  52. package/ultra-tools/commands/migrate.cjs +62 -36
  53. package/ultra-tools/commands/status.cjs +18 -9
  54. package/ultra-tools/commands/system.cjs +340 -6
  55. package/ultra-tools/commands/task.cjs +7 -1
  56. package/ultra-tools/status-command.test.cjs +29 -0
  57. package/ultra-tools/system-command.test.cjs +252 -1
@@ -11,6 +11,7 @@ const { initStateDb, closeStateDb } = require('./state-db.cjs');
11
11
  const ops = require('./state-ops.cjs');
12
12
  const changes = require('./change-workflow.cjs');
13
13
  const baselines = require('./baseline-workflow.cjs');
14
+ const archiveJournal = require('./archive-journal.cjs');
14
15
 
15
16
  function fixture() {
16
17
  const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-change-'));
@@ -24,7 +25,7 @@ function fixture() {
24
25
  db.prepare(
25
26
  `INSERT INTO baselines
26
27
  (id, project_name, mode, status, approved_by, approval_note, converged_at)
27
- VALUES ('test-baseline', 'fixture', 'migrated', 'ready', 'test', 'legacy fixture', ?)`,
28
+ VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
28
29
  ).run(new Date().toISOString());
29
30
  return { rootDir, db };
30
31
  }
@@ -130,6 +131,163 @@ test('createChange persists a change and an inspectable external-provider contex
130
131
  }
131
132
  });
132
133
 
134
+ test('new ordinary changes require a ready baseline while incidents require an explicit break-glass record', () => {
135
+ const fx = fixture();
136
+ try {
137
+ assert.throws(
138
+ () => changes.createChange(fx.db, {
139
+ id: 'chg-invalid-bypass', title: 'Ordinary work', kind: 'quick',
140
+ intent: 'Do not attach break-glass approval to ordinary work.',
141
+ baseline_bypass: { reason: 'Not applicable.', approved_by: 'owner' },
142
+ }, { rootDir: fx.rootDir }),
143
+ (error) => error.code === 'VALIDATION_ERROR',
144
+ );
145
+ fx.db.prepare('DELETE FROM baselines').run();
146
+ assert.throws(
147
+ () => changes.createChange(fx.db, {
148
+ id: 'chg-normal-without-baseline', title: 'Do ordinary work', kind: 'quick',
149
+ intent: 'Do not start ordinary work before adoption.',
150
+ }, { rootDir: fx.rootDir }),
151
+ (error) => error.code === 'BASELINE_NOT_READY',
152
+ );
153
+ assert.throws(
154
+ () => changes.createChange(fx.db, {
155
+ id: 'chg-incident-without-approval', title: 'Recover production', kind: 'incident',
156
+ intent: 'Restore the public runtime without waiting for adoption.',
157
+ }, { rootDir: fx.rootDir }),
158
+ (error) => error.code === 'BASELINE_BYPASS_REQUIRED',
159
+ );
160
+
161
+ const created = changes.createChange(fx.db, {
162
+ id: 'chg-incident-break-glass', title: 'Recover production', kind: 'incident',
163
+ intent: 'Restore the public runtime without waiting for adoption.',
164
+ baseline_bypass: {
165
+ reason: 'Production is unavailable and baseline adoption cannot precede recovery.',
166
+ approved_by: 'incident-commander',
167
+ },
168
+ }, { rootDir: fx.rootDir });
169
+ assert.equal(created.change.baseline_bypass.approved_by, 'incident-commander');
170
+ assert.equal(created.change.baseline_bypass.mode, 'incident_break_glass');
171
+ assert.match(created.change.baseline_bypass.recorded_at, /^\d{4}-\d{2}-\d{2}T/);
172
+ } finally { cleanup(fx); }
173
+ });
174
+
175
+ test('task creation authority rejects missing and terminal change ownership even with a healthy baseline', () => {
176
+ const fx = fixture();
177
+ try {
178
+ assert.throws(
179
+ () => changes.assertTaskCreationAllowed(fx.db, { change_id: 'missing-change' }, { rootDir: fx.rootDir }),
180
+ (error) => error.code === 'CHANGE_NOT_FOUND',
181
+ );
182
+ for (const status of ['ready', 'archived', 'cancelled']) {
183
+ const id = `terminal-${status}`;
184
+ changes.createChange(fx.db, {
185
+ id, title: `Terminal ${status}`, kind: 'quick',
186
+ intent: 'Verify terminal changes cannot receive new tasks.',
187
+ }, { rootDir: fx.rootDir });
188
+ fx.db.prepare('UPDATE changes SET status = ? WHERE id = ?').run(status, id);
189
+ assert.throws(
190
+ () => changes.assertTaskCreationAllowed(fx.db, { change_id: id }, { rootDir: fx.rootDir }),
191
+ (error) => error.code === 'CHANGE_NOT_MUTABLE',
192
+ );
193
+ }
194
+ } finally { cleanup(fx); }
195
+ });
196
+
197
+ test('incident break-glass remains executable and archives with a mandatory baseline reconciliation gap', () => {
198
+ const fx = fixture();
199
+ try {
200
+ fx.db.prepare(
201
+ "UPDATE baselines SET mode = 'brownfield', status = 'adopting' WHERE id = 'test-baseline'",
202
+ ).run();
203
+ const created = changes.createChange(fx.db, {
204
+ id: 'chg-break-glass-closure', title: 'Restore the production path', kind: 'incident',
205
+ intent: 'Repair the urgent runtime failure before baseline adoption can finish.',
206
+ docs_impact: { status: 'none', files: [], rationale: 'Internal runtime recovery.' },
207
+ baseline_bypass: {
208
+ reason: 'Production recovery cannot wait for the incomplete brownfield baseline.',
209
+ approved_by: 'incident-commander',
210
+ },
211
+ }, { rootDir: fx.rootDir });
212
+ ops.createTask(fx.db, {
213
+ id: 'break-glass-task', title: 'Repair urgent runtime failure', type: 'bugfix', priority: 'P0',
214
+ change_id: 'chg-break-glass-closure',
215
+ });
216
+ ops.updateTaskStatus(fx.db, 'break-glass-task', 'in_progress');
217
+ ops.updateTaskStatus(fx.db, 'break-glass-task', 'completed');
218
+ const compiled = changes.compileContext(fx.db, {
219
+ id: 'chg-break-glass-closure', ...executionContext('break-glass-task'),
220
+ }, { rootDir: fx.rootDir });
221
+ assert.equal(compiled.manifest.readiness.status, 'ready');
222
+ assert.ok(compiled.manifest.readiness.warnings.includes('BASELINE_NOT_READY:adopting'));
223
+
224
+ const diagnosisPath = path.join(path.dirname(created.intent_path), 'diagnosis.md');
225
+ fs.writeFileSync(diagnosisPath, [
226
+ '# Incident diagnosis: Restore the production path', '',
227
+ '## Reproduction', '', 'The public runtime fails on the terminal transition.', '',
228
+ '## Hypotheses', '', 'The transition loses its durable terminal write.', '',
229
+ '## Root cause', '', 'The terminal path returns before the durable write completes.', '',
230
+ '## Regression test', '', 'The regression test reproduces the lost write and then passes.', '',
231
+ '## Recovery', '', 'Replay the terminal write and verify the public runtime.', '',
232
+ ].join('\n'));
233
+
234
+ const converged = changes.convergeChange(fx.db, {
235
+ id: 'chg-break-glass-closure', evidence: incidentEvidence(),
236
+ }, { rootDir: fx.rootDir });
237
+ assert.equal(converged.ready, true);
238
+
239
+ const archived = changes.archiveChange(fx.db, {
240
+ id: 'chg-break-glass-closure', summary: 'Production restored with regression evidence.',
241
+ no_baseline_change_reason: 'Baseline adoption is tracked by the reconciliation gap.',
242
+ }, { rootDir: fx.rootDir });
243
+ assert.equal(archived.change.status, 'archived');
244
+ assert.equal(archived.baseline_bypass, true);
245
+ const baseline = baselines.readBaseline(fx.db, 'test-baseline');
246
+ assert.equal(baseline.status, 'adopting');
247
+ assert.deepEqual(
248
+ baseline.gaps.map((gap) => ({ id: gap.id, category: gap.category, status: gap.status, blocking: gap.blocking })),
249
+ [{
250
+ id: 'incident-chg-break-glass-closure-reconciliation',
251
+ category: 'baseline_blocker', status: 'open', blocking: true,
252
+ }],
253
+ );
254
+ assert.equal(baseline.gaps[0].owner, 'incident-commander');
255
+ assert.ok(baseline.gaps[0].evidence_refs.some((ref) => ref.includes('changes/archive/')));
256
+ } finally { cleanup(fx); }
257
+ });
258
+
259
+ test('an already-active ordinary change remains executable when baseline adoption later becomes incomplete', () => {
260
+ const fx = fixture();
261
+ try {
262
+ changes.createChange(fx.db, {
263
+ id: 'chg-grandfathered-context', title: 'Finish active work', kind: 'quick',
264
+ intent: 'Preserve an already-active bounded change while baseline recovery runs.',
265
+ }, { rootDir: fx.rootDir });
266
+ ops.createTask(fx.db, {
267
+ id: 'grandfathered-task', title: 'Finish active work', type: 'bugfix', priority: 'P0',
268
+ change_id: 'chg-grandfathered-context',
269
+ });
270
+ fx.db.prepare(
271
+ "UPDATE baselines SET status = 'adopting', mode = 'brownfield' WHERE id = 'test-baseline'",
272
+ ).run();
273
+
274
+ assert.doesNotThrow(() => changes.assertTaskCreationAllowed(fx.db, {
275
+ change_id: 'chg-grandfathered-context',
276
+ }, { rootDir: fx.rootDir }));
277
+ assert.throws(
278
+ () => changes.assertTaskCreationAllowed(fx.db, {}, { rootDir: fx.rootDir }),
279
+ (error) => error.code === 'BASELINE_NOT_READY',
280
+ );
281
+
282
+ const compiled = changes.compileContext(fx.db, {
283
+ id: 'chg-grandfathered-context', ...executionContext('grandfathered-task'),
284
+ }, { rootDir: fx.rootDir });
285
+ assert.equal(compiled.manifest.readiness.status, 'ready');
286
+ assert.ok(compiled.manifest.readiness.warnings.includes('BASELINE_NOT_READY:adopting'));
287
+ assert.deepEqual(compiled.manifest.readiness.blockers, []);
288
+ } finally { cleanup(fx); }
289
+ });
290
+
133
291
  test('convergeChange blocks incomplete work and marks a fully evidenced standard change ready', () => {
134
292
  const fx = fixture();
135
293
  try {
@@ -220,6 +378,27 @@ test('convergeChange requires an approved baseline after active work has remaine
220
378
  }
221
379
  });
222
380
 
381
+ test('change creation rejects a schema-migration compatibility baseline until re-adoption', () => {
382
+ const fx = fixture();
383
+ try {
384
+ fx.db.prepare(
385
+ `UPDATE baselines SET mode = 'migrated', approved_by = 'schema-migration',
386
+ approval_note = 'compatibility only' WHERE id = 'test-baseline'`,
387
+ ).run();
388
+ assert.throws(
389
+ () => changes.createChange(fx.db, {
390
+ id: 'chg-migrated-gate', title: 'Require owner adoption', kind: 'quick',
391
+ intent: 'Do not let schema migration impersonate project-owner approval.',
392
+ docs_impact: { status: 'none', files: [], rationale: 'Gate behavior only.' },
393
+ }, { rootDir: fx.rootDir }),
394
+ (error) => error.code === 'BASELINE_NOT_READY'
395
+ && error.details.blockers.includes('BASELINE_MIGRATION_REVIEW_REQUIRED'),
396
+ );
397
+ } finally {
398
+ cleanup(fx);
399
+ }
400
+ });
401
+
223
402
  test('archiveChange moves a ready change into immutable history and records baseline reconciliation', () => {
224
403
  const fx = fixture();
225
404
  try {
@@ -385,6 +564,61 @@ test('archiveChange rolls back when declared reconciliation leaves a tracked spe
385
564
  }
386
565
  });
387
566
 
567
+ test('archiveChange resumes a durable journal after the filesystem move outlives the process', () => {
568
+ const fx = fixture();
569
+ try {
570
+ changes.createChange(fx.db, {
571
+ id: 'chg-crash-resume', title: 'Resume interrupted archive', kind: 'quick',
572
+ intent: 'Finish an authorized archive after a process crash.',
573
+ docs_impact: { status: 'none', files: [], rationale: 'Recovery-only behavior.' },
574
+ }, { rootDir: fx.rootDir });
575
+ ops.createTask(fx.db, {
576
+ id: 'crash-resume-task', title: 'Verify archive recovery', type: 'bugfix', priority: 'P0',
577
+ change_id: 'chg-crash-resume',
578
+ });
579
+ ops.updateTaskStatus(fx.db, 'crash-resume-task', 'in_progress');
580
+ ops.updateTaskStatus(fx.db, 'crash-resume-task', 'completed');
581
+ changes.compileContext(fx.db, {
582
+ id: 'chg-crash-resume', ...executionContext('crash-resume-task'),
583
+ }, { rootDir: fx.rootDir });
584
+ const converged = changes.convergeChange(fx.db, {
585
+ id: 'chg-crash-resume',
586
+ evidence: [
587
+ { category: 'diff', status: 'pass', evidence: 'recovery diff reviewed' },
588
+ {
589
+ category: 'tests', status: 'pass', evidence: 'crash recovery passed', seam: 'archive retry',
590
+ signal: {
591
+ command: 'node --test archive-journal.test.cjs',
592
+ expected_red: 'archive retry fails after the filesystem move',
593
+ observed_red: true, observed_green: true, deterministic: true, duration_ms: 10,
594
+ },
595
+ },
596
+ { category: 'spec', status: 'not_applicable', evidence: 'No contract change.' },
597
+ ],
598
+ }, { rootDir: fx.rootDir });
599
+ assert.equal(converged.ready, true);
600
+ const input = {
601
+ id: 'chg-crash-resume', summary: 'Resume the interrupted archive safely.',
602
+ no_baseline_change_reason: 'No baseline content changed.',
603
+ };
604
+ const change = changes.readChange(fx.db, input.id);
605
+ const interrupted = archiveJournal.prepareArchiveMove({
606
+ rootDir: fx.rootDir, change, summary: input.summary, baselineUpdates: [],
607
+ noBaselineChangeReason: input.no_baseline_change_reason,
608
+ });
609
+ assert.equal(changes.readChange(fx.db, input.id).status, 'ready');
610
+ assert.equal(fs.existsSync(interrupted.source), false);
611
+ assert.equal(fs.existsSync(interrupted.destination), true);
612
+
613
+ const resumed = changes.archiveChange(fx.db, input, { rootDir: fx.rootDir });
614
+ assert.equal(resumed.change.status, 'archived');
615
+ assert.equal(resumed.archive_path, interrupted.destination);
616
+ assert.equal(archiveJournal.listArchiveIntents(fx.rootDir).length, 0);
617
+ } finally {
618
+ cleanup(fx);
619
+ }
620
+ });
621
+
388
622
  test('provider references reject embedded memory or graph payloads', () => {
389
623
  const fx = fixture();
390
624
  try {
@@ -52,6 +52,11 @@ test('a task is not plan-ready before its execution contract is compiled', () =>
52
52
  const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-spine-plan-'));
53
53
  const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
54
54
  try {
55
+ db.prepare(
56
+ `INSERT INTO baselines
57
+ (id, project_name, mode, status, approved_by, approval_note, converged_at)
58
+ VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
59
+ ).run(new Date().toISOString());
55
60
  const { change } = createChange(db, {
56
61
  id: 'planning-contract', title: 'Planning contract', kind: 'standard',
57
62
  intent: 'Require an executable fresh-context slice before implementation.',
@@ -9,6 +9,8 @@ const runtime = require('./runtime-state.cjs');
9
9
  const projector = require('./projector.cjs');
10
10
  const recovery = require('../../orchestrator/recovery.cjs');
11
11
  const baselines = require('./baseline-workflow.cjs');
12
+ const changes = require('./change-workflow.cjs');
13
+ const archiveJournal = require('./archive-journal.cjs');
12
14
 
13
15
  const SPEC_CONSUMER = 'spec-staleness';
14
16
 
@@ -49,11 +51,14 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
49
51
  WHERE status IN ('active', 'blocked', 'ready') AND artifact_root IS NOT NULL`,
50
52
  ).all().filter((row) => row.artifact_root && !fs.existsSync(path.resolve(rootDir, row.artifact_root))).length
51
53
  : 0;
54
+ const archiveIntents = missing.length === 0 ? archiveJournal.listArchiveIntents(rootDir) : [];
55
+ const corruptArchiveIntents = archiveIntents.filter((item) => item.error).length;
52
56
  const baseline = missing.length === 0
53
57
  ? baselines.inspectBaseline(db, { rootDir })
54
58
  : { status: 'fail', blockers: ['BASELINE_STATE_UNAVAILABLE'], warnings: [], baseline: null };
55
59
  const degraded = integrity !== 'ok' || missing.length > 0 || incidents.length > 0
56
60
  || pending.length > 0 || running.length > 0 || failed.length > 0 || orphanSessions > 0 || activeMissing > 0
61
+ || archiveIntents.length > 0
57
62
  || eventCursor > projectedCursor;
58
63
  return {
59
64
  status: degraded ? 'degraded' : 'healthy',
@@ -67,6 +72,10 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
67
72
  },
68
73
  sessions: { status: orphanSessions === 0 ? 'pass' : 'fail', orphan: orphanSessions },
69
74
  change_artifacts: { status: activeMissing === 0 ? 'pass' : 'fail', missing: activeMissing },
75
+ archive_recovery: {
76
+ status: archiveIntents.length === 0 ? 'pass' : 'fail',
77
+ pending: archiveIntents.length, corrupt: corruptArchiveIntents,
78
+ },
70
79
  baseline: {
71
80
  status: baseline.status === 'pass' ? 'pass' : 'warning',
72
81
  readiness: baseline.status,
@@ -74,6 +83,9 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
74
83
  baseline_status: baseline.baseline?.status || null,
75
84
  id: baseline.baseline?.id || null,
76
85
  repository_revision: baseline.baseline?.repository_revision || null,
86
+ repository_branch: baseline.baseline?.repository_branch || null,
87
+ worktree_state: baseline.baseline?.worktree_state || 'unavailable',
88
+ gaps: baseline.baseline ? baselines.summarizeGaps(baseline.baseline.gaps) : null,
77
89
  blockers: baseline.blockers,
78
90
  warnings: baseline.warnings,
79
91
  },
@@ -100,6 +112,7 @@ async function runDoctor(db, {
100
112
  } = {}) {
101
113
  if (!repair) return { ...inspectSystem(db, { rootDir }), repair_performed: false };
102
114
  const backupPath = await backupDatabase(db, rootDir);
115
+ const archives = changes.recoverInterruptedArchives(db, { rootDir });
103
116
  const recovered = recovery.recoverOnBoot(db);
104
117
  const cursor = runtime.readConsumerCursor(db, SPEC_CONSUMER);
105
118
  const consumed = ops.consumeSpecChangedEvents(db, { since_id: cursor, limit: 500 });
@@ -114,7 +127,7 @@ async function runDoctor(db, {
114
127
  ...inspectSystem(db, { rootDir }),
115
128
  repair_performed: true,
116
129
  backup_path: backupPath,
117
- repair: { recovered, consumed, interrupted, requeued, ensured, projections },
130
+ repair: { archives, recovered, consumed, interrupted, requeued, ensured, projections },
118
131
  };
119
132
  }
120
133
 
@@ -11,15 +11,17 @@ const ops = require('./state-ops.cjs');
11
11
  const runtime = require('./runtime-state.cjs');
12
12
  const doctor = require('./doctor.cjs');
13
13
  const baselines = require('./baseline-workflow.cjs');
14
+ const changes = require('./change-workflow.cjs');
15
+ const archiveJournal = require('./archive-journal.cjs');
14
16
 
15
- function fixture({ migratedBaseline = true } = {}) {
17
+ function fixture({ baseline = true } = {}) {
16
18
  const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-doctor-'));
17
19
  const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
18
- if (migratedBaseline) {
20
+ if (baseline) {
19
21
  db.prepare(
20
22
  `INSERT INTO baselines
21
23
  (id, project_name, mode, status, approved_by, approval_note, converged_at)
22
- VALUES ('test-baseline', 'fixture', 'migrated', 'ready', 'test', 'legacy fixture', ?)`,
24
+ VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
23
25
  ).run(new Date().toISOString());
24
26
  }
25
27
  return { rootDir, db };
@@ -44,7 +46,7 @@ test('doctor reports structured health for an initialized Ultra project', async
44
46
  });
45
47
 
46
48
  test('doctor reports incomplete brownfield adoption as advisory rather than authority failure', async () => {
47
- const fx = fixture({ migratedBaseline: false });
49
+ const fx = fixture({ baseline: false });
48
50
  try {
49
51
  baselines.startBaseline(fx.db, {
50
52
  id: 'adoption', project_name: 'legacy', mode: 'brownfield', scope: ['.'],
@@ -53,6 +55,9 @@ test('doctor reports incomplete brownfield adoption as advisory rather than auth
53
55
  assert.equal(report.status, 'healthy');
54
56
  assert.equal(report.checks.baseline.status, 'warning');
55
57
  assert.equal(report.checks.baseline.mode, 'brownfield');
58
+ assert.deepEqual(report.checks.baseline.gaps, {
59
+ total: 0, open: 0, blocking: 0, by_category: {},
60
+ });
56
61
  assert.ok(report.checks.baseline.blockers.includes('BASELINE_NOT_READY:adopting'));
57
62
  } finally {
58
63
  cleanup(fx);
@@ -108,3 +113,36 @@ test('doctor reports running projection work and repairs only stale interrupted
108
113
  cleanup(fx);
109
114
  }
110
115
  });
116
+
117
+ test('doctor repair resumes an archive journal left after a process crash', async () => {
118
+ const fx = fixture();
119
+ try {
120
+ const activeRoot = path.join(fx.rootDir, '.ultra', 'changes', 'active', 'doctor-archive');
121
+ fs.mkdirSync(activeRoot, { recursive: true });
122
+ fs.writeFileSync(path.join(activeRoot, 'intent.md'), '# Authorized archive\n');
123
+ fx.db.prepare(
124
+ `INSERT INTO changes
125
+ (id, title, kind, status, intent, docs_impact_json, provider_refs_json, artifact_root)
126
+ VALUES ('doctor-archive', 'Recover archive', 'quick', 'ready', 'Recover it',
127
+ '{"status":"none","files":[],"rationale":"recovery"}', '{}',
128
+ '.ultra/changes/active/doctor-archive')`,
129
+ ).run();
130
+ const prepared = archiveJournal.prepareArchiveMove({
131
+ rootDir: fx.rootDir, change: changes.readChange(fx.db, 'doctor-archive'),
132
+ summary: 'Resume the authorized archive.', baselineUpdates: [],
133
+ noBaselineChangeReason: 'No baseline content changed.',
134
+ });
135
+ assert.equal(fs.existsSync(prepared.source), false);
136
+ const before = doctor.inspectSystem(fx.db, { rootDir: fx.rootDir });
137
+ assert.equal(before.status, 'degraded');
138
+ assert.equal(before.checks.archive_recovery.pending, 1);
139
+
140
+ const after = await doctor.runDoctor(fx.db, { rootDir: fx.rootDir, repair: true });
141
+ assert.equal(after.repair.archives.resumed, 1);
142
+ assert.equal(changes.readChange(fx.db, 'doctor-archive').status, 'archived');
143
+ assert.equal(after.checks.archive_recovery.pending, 0);
144
+ assert.equal(fs.existsSync(path.join(prepared.destination, archiveJournal.INTENT_FILE)), false);
145
+ } finally {
146
+ cleanup(fx);
147
+ }
148
+ });