ultra-builder-pro-cli 0.11.0 → 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 (75) hide show
  1. package/.ultra-template/docs/research/README.md +21 -44
  2. package/.ultra-template/specs/architecture.md +54 -223
  3. package/.ultra-template/specs/discovery.md +41 -228
  4. package/.ultra-template/specs/product.md +44 -200
  5. package/.ultra-template/tasks/contexts/TEMPLATE.md +36 -0
  6. package/.ultra-template/tasks/tasks.json +6 -0
  7. package/CHANGELOG.md +94 -1
  8. package/README.md +37 -9
  9. package/adapters/_shared/codex-assets.cjs +1 -1
  10. package/adapters/_shared/handbook.cjs +5 -4
  11. package/adapters/_shared/tests/handbook.test.cjs +4 -0
  12. package/adapters/opencode.js +8 -2
  13. package/adapters/tests/codex.test.cjs +10 -2
  14. package/adapters/tests/kimi-hook.test.cjs +1 -1
  15. package/adapters/tests/opencode.test.cjs +36 -1
  16. package/commands/ultra-init.md +3 -3
  17. package/docs/AGENT-CONTEXT.md +17 -8
  18. package/docs/ARCHITECTURE.md +51 -8
  19. package/docs/PLAN.zh-CN.md +9 -6
  20. package/docs/ROADMAP.md +5 -3
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +8 -5
  22. package/docs/STATE-DB-ACCESS-POLICY.md +19 -13
  23. package/docs/USER-HANDBOOK-CONTRACT.md +9 -5
  24. package/hooks/README.md +4 -3
  25. package/hooks/adapters/codex.py +1 -1
  26. package/hooks/adapters/kimi.py +1 -1
  27. package/hooks/context_spine.py +153 -16
  28. package/hooks/health_check.py +11 -0
  29. package/hooks/pre_stop_check.py +6 -5
  30. package/mcp-server/lib/archive-journal.cjs +192 -0
  31. package/mcp-server/lib/archive-journal.test.cjs +35 -0
  32. package/mcp-server/lib/baseline-workflow.cjs +781 -0
  33. package/mcp-server/lib/baseline-workflow.test.cjs +477 -0
  34. package/mcp-server/lib/change-workflow.cjs +256 -85
  35. package/mcp-server/lib/change-workflow.test.cjs +436 -6
  36. package/mcp-server/lib/context-spine.cjs +74 -9
  37. package/mcp-server/lib/context-spine.test.cjs +20 -0
  38. package/mcp-server/lib/doctor.cjs +28 -1
  39. package/mcp-server/lib/doctor.test.cjs +63 -1
  40. package/mcp-server/lib/init-project.cjs +334 -16
  41. package/mcp-server/lib/provider-refs.cjs +42 -0
  42. package/mcp-server/lib/spec-learning.test.cjs +5 -0
  43. package/mcp-server/lib/state-authority.cjs +9 -3
  44. package/mcp-server/lib/state-db.cjs +202 -21
  45. package/mcp-server/lib/state-db.test.cjs +140 -3
  46. package/mcp-server/lib/state-ops.cjs +68 -6
  47. package/mcp-server/lib/task-expander.cjs +7 -1
  48. package/mcp-server/lib/task-expander.test.cjs +71 -0
  49. package/mcp-server/server.cjs +62 -6
  50. package/mcp-server/tests/init-project.test.cjs +216 -9
  51. package/mcp-server/tests/migrate.test.cjs +33 -0
  52. package/mcp-server/tests/server-contract.test.cjs +478 -3
  53. package/mcp-server/tests/state-db.test.cjs +23 -2
  54. package/mcp-server/tests/state-ops.test.cjs +108 -0
  55. package/package.json +1 -1
  56. package/skills/ultra-change/SKILL.md +32 -13
  57. package/skills/ultra-deliver/SKILL.md +19 -5
  58. package/skills/ultra-dev/SKILL.md +8 -2
  59. package/skills/ultra-doctor/SKILL.md +30 -9
  60. package/skills/ultra-init/SKILL.md +107 -66
  61. package/skills/ultra-init/references/brownfield-adoption.md +93 -0
  62. package/skills/ultra-plan/SKILL.md +15 -5
  63. package/skills/ultra-research/SKILL.md +17 -5
  64. package/skills/ultra-status/SKILL.md +15 -6
  65. package/spec/cli-protocol.md +15 -1
  66. package/spec/mcp-tools.yaml +316 -11
  67. package/spec/schemas/mcp-tools.schema.json +1 -1
  68. package/spec/schemas/state-db.sql +44 -2
  69. package/ultra-tools/cli.cjs +9 -5
  70. package/ultra-tools/commands/migrate.cjs +62 -36
  71. package/ultra-tools/commands/status.cjs +29 -2
  72. package/ultra-tools/commands/system.cjs +340 -6
  73. package/ultra-tools/commands/task.cjs +11 -2
  74. package/ultra-tools/status-command.test.cjs +49 -0
  75. package/ultra-tools/system-command.test.cjs +256 -0
@@ -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' };
@@ -37,6 +52,11 @@ test('a task is not plan-ready before its execution contract is compiled', () =>
37
52
  const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-spine-plan-'));
38
53
  const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
39
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());
40
60
  const { change } = createChange(db, {
41
61
  id: 'planning-contract', title: 'Planning contract', kind: 'standard',
42
62
  intent: 'Require an executable fresh-context slice before implementation.',
@@ -8,6 +8,9 @@ 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');
12
+ const changes = require('./change-workflow.cjs');
13
+ const archiveJournal = require('./archive-journal.cjs');
11
14
 
12
15
  const SPEC_CONSUMER = 'spec-staleness';
13
16
 
@@ -48,8 +51,14 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
48
51
  WHERE status IN ('active', 'blocked', 'ready') AND artifact_root IS NOT NULL`,
49
52
  ).all().filter((row) => row.artifact_root && !fs.existsSync(path.resolve(rootDir, row.artifact_root))).length
50
53
  : 0;
54
+ const archiveIntents = missing.length === 0 ? archiveJournal.listArchiveIntents(rootDir) : [];
55
+ const corruptArchiveIntents = archiveIntents.filter((item) => item.error).length;
56
+ const baseline = missing.length === 0
57
+ ? baselines.inspectBaseline(db, { rootDir })
58
+ : { status: 'fail', blockers: ['BASELINE_STATE_UNAVAILABLE'], warnings: [], baseline: null };
51
59
  const degraded = integrity !== 'ok' || missing.length > 0 || incidents.length > 0
52
60
  || pending.length > 0 || running.length > 0 || failed.length > 0 || orphanSessions > 0 || activeMissing > 0
61
+ || archiveIntents.length > 0
53
62
  || eventCursor > projectedCursor;
54
63
  return {
55
64
  status: degraded ? 'degraded' : 'healthy',
@@ -63,6 +72,23 @@ function inspectSystem(db, { rootDir = process.cwd() } = {}) {
63
72
  },
64
73
  sessions: { status: orphanSessions === 0 ? 'pass' : 'fail', orphan: orphanSessions },
65
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
+ },
79
+ baseline: {
80
+ status: baseline.status === 'pass' ? 'pass' : 'warning',
81
+ readiness: baseline.status,
82
+ mode: baseline.baseline?.mode || null,
83
+ baseline_status: baseline.baseline?.status || null,
84
+ id: baseline.baseline?.id || null,
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,
89
+ blockers: baseline.blockers,
90
+ warnings: baseline.warnings,
91
+ },
66
92
  external_providers: {
67
93
  status: 'pass', ownership: 'external',
68
94
  note: 'Ultra stores provider metadata references only and never owns memory or code-graph content.',
@@ -86,6 +112,7 @@ async function runDoctor(db, {
86
112
  } = {}) {
87
113
  if (!repair) return { ...inspectSystem(db, { rootDir }), repair_performed: false };
88
114
  const backupPath = await backupDatabase(db, rootDir);
115
+ const archives = changes.recoverInterruptedArchives(db, { rootDir });
89
116
  const recovered = recovery.recoverOnBoot(db);
90
117
  const cursor = runtime.readConsumerCursor(db, SPEC_CONSUMER);
91
118
  const consumed = ops.consumeSpecChangedEvents(db, { since_id: cursor, limit: 500 });
@@ -100,7 +127,7 @@ async function runDoctor(db, {
100
127
  ...inspectSystem(db, { rootDir }),
101
128
  repair_performed: true,
102
129
  backup_path: backupPath,
103
- repair: { recovered, consumed, interrupted, requeued, ensured, projections },
130
+ repair: { archives, recovered, consumed, interrupted, requeued, ensured, projections },
104
131
  };
105
132
  }
106
133
 
@@ -10,10 +10,20 @@ 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');
14
+ const changes = require('./change-workflow.cjs');
15
+ const archiveJournal = require('./archive-journal.cjs');
13
16
 
14
- function fixture() {
17
+ function fixture({ baseline = true } = {}) {
15
18
  const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-doctor-'));
16
19
  const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
20
+ if (baseline) {
21
+ db.prepare(
22
+ `INSERT INTO baselines
23
+ (id, project_name, mode, status, approved_by, approval_note, converged_at)
24
+ VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
25
+ ).run(new Date().toISOString());
26
+ }
17
27
  return { rootDir, db };
18
28
  }
19
29
 
@@ -35,6 +45,25 @@ test('doctor reports structured health for an initialized Ultra project', async
35
45
  }
36
46
  });
37
47
 
48
+ test('doctor reports incomplete brownfield adoption as advisory rather than authority failure', async () => {
49
+ const fx = fixture({ baseline: false });
50
+ try {
51
+ baselines.startBaseline(fx.db, {
52
+ id: 'adoption', project_name: 'legacy', mode: 'brownfield', scope: ['.'],
53
+ }, { rootDir: fx.rootDir, emitEvent: false });
54
+ const report = await doctor.runDoctor(fx.db, { rootDir: fx.rootDir });
55
+ assert.equal(report.status, 'healthy');
56
+ assert.equal(report.checks.baseline.status, 'warning');
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
+ });
61
+ assert.ok(report.checks.baseline.blockers.includes('BASELINE_NOT_READY:adopting'));
62
+ } finally {
63
+ cleanup(fx);
64
+ }
65
+ });
66
+
38
67
  test('doctor repair is backup-first and drains projection/spec-event recovery work', async () => {
39
68
  const fx = fixture();
40
69
  try {
@@ -84,3 +113,36 @@ test('doctor reports running projection work and repairs only stale interrupted
84
113
  cleanup(fx);
85
114
  }
86
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
+ });