ultra-builder-pro-cli 0.12.1 → 0.14.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.
- package/.ultra-template/docs/research/README.md +21 -44
- package/.ultra-template/specs/discovery.md +10 -0
- package/CHANGELOG.md +95 -1
- package/README.md +31 -16
- package/adapters/_shared/codex-assets.cjs +23 -163
- package/adapters/_shared/handbook.cjs +2 -2
- package/adapters/_shared/runtime-assets.cjs +1 -0
- package/adapters/_shared/tests/runtime-assets.test.cjs +18 -0
- package/adapters/opencode.js +30 -120
- package/adapters/tests/claude.test.cjs +3 -2
- package/adapters/tests/codex-hook.test.cjs +34 -7
- package/adapters/tests/codex.test.cjs +21 -6
- package/adapters/tests/kimi-hook.test.cjs +34 -7
- package/adapters/tests/opencode.test.cjs +70 -84
- package/commands/ultra-init.md +1 -1
- package/docs/AGENT-CONTEXT.md +10 -6
- package/docs/ARCHITECTURE.md +40 -17
- package/docs/PLAN.zh-CN.md +5 -4
- package/docs/ROADMAP.md +2 -2
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +3 -3
- package/docs/USER-HANDBOOK-CONTRACT.md +5 -4
- package/hooks/active_task_context.py +22 -50
- package/hooks/context_spine.py +61 -299
- package/hooks/health_check.py +19 -17
- package/hooks/pre_stop_check.py +28 -33
- package/hooks/subagent_tracker.py +13 -19
- package/hooks/workflow_checkpoint.py +39 -47
- package/hooks/workflow_context.py +4 -9
- package/hooks/workflow_resume.py +48 -128
- package/mcp-server/breadcrumb.cjs +22 -0
- package/mcp-server/lib/archive-journal.cjs +192 -0
- package/mcp-server/lib/archive-journal.test.cjs +35 -0
- package/mcp-server/lib/baseline-workflow.cjs +404 -38
- package/mcp-server/lib/baseline-workflow.test.cjs +252 -0
- package/mcp-server/lib/change-workflow.cjs +249 -75
- package/mcp-server/lib/change-workflow.test.cjs +235 -1
- package/mcp-server/lib/context-spine.test.cjs +5 -0
- package/mcp-server/lib/doctor.cjs +14 -1
- package/mcp-server/lib/doctor.test.cjs +42 -4
- package/mcp-server/lib/init-project.cjs +292 -9
- package/mcp-server/lib/prd-parser.cjs +14 -41
- package/mcp-server/lib/prd-parser.test.cjs +49 -138
- package/mcp-server/lib/project-breadcrumb.cjs +139 -0
- package/mcp-server/lib/project-breadcrumb.test.cjs +112 -0
- package/mcp-server/lib/spec-learning.test.cjs +1 -1
- package/mcp-server/lib/state-authority.cjs +9 -3
- package/mcp-server/lib/state-db.cjs +154 -32
- package/mcp-server/lib/state-db.test.cjs +136 -4
- package/mcp-server/lib/state-ops.cjs +68 -6
- package/mcp-server/lib/task-expander.cjs +14 -37
- package/mcp-server/lib/task-expander.test.cjs +80 -164
- package/mcp-server/server.cjs +34 -37
- package/mcp-server/tests/init-project.test.cjs +162 -1
- package/mcp-server/tests/migrate.test.cjs +33 -0
- package/mcp-server/tests/server-contract.test.cjs +391 -17
- package/mcp-server/tests/state-db.test.cjs +14 -2
- package/mcp-server/tests/state-ops.test.cjs +108 -0
- package/package.json +2 -4
- package/skills/cc-collab/SKILL.md +2 -2
- package/skills/codex-collab/SKILL.md +2 -3
- package/skills/ultra-change/SKILL.md +18 -10
- package/skills/ultra-deliver/SKILL.md +13 -9
- package/skills/ultra-dev/SKILL.md +6 -4
- package/skills/ultra-doctor/SKILL.md +22 -7
- package/skills/ultra-init/SKILL.md +104 -59
- package/skills/ultra-init/references/brownfield-adoption.md +65 -37
- package/skills/ultra-plan/SKILL.md +20 -7
- package/skills/ultra-research/SKILL.md +12 -5
- package/skills/ultra-status/SKILL.md +6 -4
- package/skills/ultra-verify/SKILL.md +21 -30
- package/skills/ultra-verify/scripts/verify_wait.py +54 -54
- package/spec/cli-protocol.md +11 -1
- package/spec/mcp-tools.yaml +133 -19
- package/spec/schemas/state-db.sql +11 -0
- package/ultra-tools/cli.cjs +9 -5
- package/ultra-tools/commands/migrate.cjs +62 -36
- package/ultra-tools/commands/status.cjs +18 -9
- package/ultra-tools/commands/system.cjs +340 -6
- package/ultra-tools/commands/task.cjs +7 -1
- package/ultra-tools/status-command.test.cjs +29 -0
- package/ultra-tools/system-command.test.cjs +252 -1
- package/mcp-server/lib/llm-client.cjs +0 -157
- package/mcp-server/lib/llm-client.test.cjs +0 -156
- package/orchestrator/planner/expand-prompt.cjs +0 -62
- package/orchestrator/planner/expand-prompt.test.cjs +0 -52
- package/orchestrator/planner/prd-prompt.cjs +0 -44
- package/orchestrator/planner/prd-prompt.test.cjs +0 -30
package/spec/mcp-tools.yaml
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
# Validated by spec/schemas/mcp-tools.schema.json (run `npm run test:spec`).
|
|
4
4
|
# Trace: PLAN §7.3, decisions D12 / D29 / D31-D34.
|
|
5
5
|
|
|
6
|
-
version: '1.
|
|
6
|
+
version: '1.2'
|
|
7
7
|
|
|
8
8
|
info:
|
|
9
9
|
title: Ultra Builder Pro MCP tool contracts
|
|
10
|
-
phase: '1.
|
|
10
|
+
phase: '1.2'
|
|
11
11
|
trace_to: 'PLAN docs/PLAN.zh-CN.md §7.3'
|
|
12
12
|
notes: |
|
|
13
13
|
This published contract contains only tools registered by the MCP server.
|
|
@@ -32,7 +32,7 @@ tools:
|
|
|
32
32
|
# ─────────────────────────── task family ────────────────────────────────
|
|
33
33
|
- name: task.create
|
|
34
34
|
family: task
|
|
35
|
-
description: Create a new task in state.db. Emits task_created event. If `id` is omitted the server mints a uuid.
|
|
35
|
+
description: Create a new task in state.db. Emits task_created event. If `id` is omitted the server mints a uuid. Child tasks inherit the parent's change ownership; conflicting ownership is rejected.
|
|
36
36
|
phase: '2'
|
|
37
37
|
writer_role: mcp
|
|
38
38
|
input_schema:
|
|
@@ -48,10 +48,10 @@ tools:
|
|
|
48
48
|
estimated_days: { type: number, exclusiveMinimum: 0, description: 'estimated implementation effort in days' }
|
|
49
49
|
deps: { type: array, items: { type: string } }
|
|
50
50
|
files_modified: { type: array, items: { type: string }, description: 'Phase 8A conflict detection' }
|
|
51
|
-
parent_id: { type: string, description: 'Phase 8A.3 parent reference
|
|
51
|
+
parent_id: { type: string, description: 'Phase 8A.3 parent reference; the child inherits this task change ownership' }
|
|
52
52
|
tag: { type: string, description: 'git branch tag (Phase 7.2)' }
|
|
53
53
|
trace_to: { type: string, description: 'spec anchor (e.g. spec/mcp-tools.yaml#task.create)' }
|
|
54
|
-
change_id: { type: string, pattern: '^[a-zA-Z0-9_\-]+$', description: 'continuous change unit owning this task' }
|
|
54
|
+
change_id: { type: string, pattern: '^[a-zA-Z0-9_\-]+$', description: 'active or blocked continuous change unit owning this task; inherited from parent_id when omitted' }
|
|
55
55
|
output_schema:
|
|
56
56
|
type: object
|
|
57
57
|
required: [id, status, created_at]
|
|
@@ -63,6 +63,11 @@ tools:
|
|
|
63
63
|
errors:
|
|
64
64
|
- { code: VALIDATION_ERROR, description: 'input failed schema validation' }
|
|
65
65
|
- { code: DUPLICATE_TASK_ID, description: 'task id already exists' }
|
|
66
|
+
- { code: TASK_NOT_FOUND, description: 'parent task does not exist' }
|
|
67
|
+
- { code: CHANGE_NOT_FOUND, description: 'assigned change does not exist' }
|
|
68
|
+
- { code: CHANGE_NOT_MUTABLE, description: 'assigned change no longer accepts tasks' }
|
|
69
|
+
- { code: TASK_CHANGE_OWNERSHIP_MISMATCH, description: 'child ownership conflicts with its parent change' }
|
|
70
|
+
- { code: BASELINE_NOT_READY, description: 'ordinary task creation requires a ready baseline or an already-authorized active change' }
|
|
66
71
|
- { code: LEGACY_STATE_MIGRATION_REQUIRED, description: 'non-empty v4.4 projection must be migrated before state-backed tools can run' }
|
|
67
72
|
- { code: STATE_DB_LOCKED, description: 'database is locked', retriable: true }
|
|
68
73
|
cli_subcommand: task create
|
|
@@ -106,6 +111,9 @@ tools:
|
|
|
106
111
|
- { code: TASK_NOT_FOUND, description: 'no task with given id' }
|
|
107
112
|
- { code: ILLEGAL_STATUS_TRANSITION, description: 'transition rejected by state machine' }
|
|
108
113
|
- { code: VALIDATION_ERROR, description: 'patch failed schema validation' }
|
|
114
|
+
- { code: CHANGE_NOT_FOUND, description: 'assigned change does not exist' }
|
|
115
|
+
- { code: CHANGE_NOT_MUTABLE, description: 'assigned change no longer accepts task updates' }
|
|
116
|
+
- { code: TASK_CHANGE_OWNERSHIP_MISMATCH, description: 'task ownership conflicts with its parent or children' }
|
|
109
117
|
- { code: LEGACY_STATE_MIGRATION_REQUIRED, description: 'non-empty v4.4 projection must be migrated before state-backed tools can run' }
|
|
110
118
|
cli_subcommand: task update
|
|
111
119
|
emits_events: [task_started, task_completed, task_blocked, task_stale_marked]
|
|
@@ -246,6 +254,17 @@ tools:
|
|
|
246
254
|
enum: [auto, greenfield, brownfield]
|
|
247
255
|
default: auto
|
|
248
256
|
description: auto detects whether meaningful project content already exists
|
|
257
|
+
resume:
|
|
258
|
+
type: boolean
|
|
259
|
+
default: false
|
|
260
|
+
description: preserve an existing `.ultra/`, upgrade its schema, and install only missing current scaffold files
|
|
261
|
+
scope:
|
|
262
|
+
type: array
|
|
263
|
+
minItems: 1
|
|
264
|
+
uniqueItems: true
|
|
265
|
+
items: { type: string, minLength: 1 }
|
|
266
|
+
default: ['.']
|
|
267
|
+
description: project-relative repository roots governed by the initial baseline
|
|
249
268
|
overwrite:
|
|
250
269
|
type: boolean
|
|
251
270
|
default: false
|
|
@@ -255,7 +274,7 @@ tools:
|
|
|
255
274
|
description: override path to the template root (defaults to bundled templates/.ultra/)
|
|
256
275
|
output_schema:
|
|
257
276
|
type: object
|
|
258
|
-
required: [created_path, state_db_path, status, mode, baseline, copied_files]
|
|
277
|
+
required: [created_path, state_db_path, status, mode, baseline, copied_files, repository_profile]
|
|
259
278
|
additionalProperties: false
|
|
260
279
|
properties:
|
|
261
280
|
created_path:
|
|
@@ -266,43 +285,66 @@ tools:
|
|
|
266
285
|
description: absolute path to the initialized authoritative SQLite database
|
|
267
286
|
status:
|
|
268
287
|
type: string
|
|
269
|
-
enum: [created, overwritten]
|
|
288
|
+
enum: [created, overwritten, resumed]
|
|
270
289
|
mode:
|
|
271
290
|
type: string
|
|
272
291
|
enum: [greenfield, brownfield]
|
|
273
292
|
baseline:
|
|
274
293
|
type: object
|
|
275
|
-
required: [id, mode, status]
|
|
294
|
+
required: [id, mode, status, scope]
|
|
276
295
|
additionalProperties: true
|
|
277
296
|
copied_files:
|
|
278
297
|
type: array
|
|
279
298
|
items: { type: string }
|
|
280
299
|
description: paths relative to created_path
|
|
300
|
+
repository_profile:
|
|
301
|
+
type: object
|
|
302
|
+
description: bounded classification evidence used to route greenfield or brownfield adoption
|
|
281
303
|
backup_path:
|
|
282
304
|
type: string
|
|
283
305
|
description: set only when status=overwritten
|
|
306
|
+
migration_backup_path:
|
|
307
|
+
type: string
|
|
308
|
+
description: pre-migration database backup created while resuming an older schema
|
|
284
309
|
errors:
|
|
285
310
|
- { code: ULTRA_DIR_EXISTS, description: '.ultra/ already exists and overwrite=false' }
|
|
286
311
|
- { code: TEMPLATE_MISSING, description: 'source_template path does not exist' }
|
|
287
312
|
- { code: TARGET_NOT_DIR, description: 'target_dir is not a directory' }
|
|
313
|
+
- { code: BASELINE_SCOPE_MISSING, description: 'a selected baseline scope does not exist' }
|
|
314
|
+
- { code: LEGACY_STATE_MIGRATION_REQUIRED, description: 'projection-only tasks must be imported before resume' }
|
|
315
|
+
- { code: STATE_AUTHORITY_CONFLICT, description: 'projection tasks and authoritative state cannot be reconciled automatically' }
|
|
316
|
+
- { code: STATE_PROJECTION_INVALID, description: 'the existing task projection is malformed' }
|
|
288
317
|
- { code: IO_ERROR, description: 'filesystem write failed', retriable: true }
|
|
289
318
|
cli_subcommand: task init-project
|
|
290
|
-
emits_events: [project_initialized]
|
|
319
|
+
emits_events: [project_initialized, project_resumed]
|
|
291
320
|
trace_to: 'PLAN §6 Phase 3.1'
|
|
292
321
|
|
|
293
322
|
- name: task.expand
|
|
294
323
|
family: task
|
|
295
|
-
description:
|
|
324
|
+
description: Validate and atomically persist child tasks derived by the current host from one parent task.
|
|
296
325
|
phase: '8a'
|
|
297
326
|
writer_role: mcp
|
|
298
327
|
input_schema:
|
|
299
328
|
type: object
|
|
300
|
-
required: [id]
|
|
329
|
+
required: [id, children]
|
|
301
330
|
additionalProperties: false
|
|
302
331
|
properties:
|
|
303
|
-
id:
|
|
304
|
-
|
|
305
|
-
|
|
332
|
+
id: { type: string }
|
|
333
|
+
children:
|
|
334
|
+
type: array
|
|
335
|
+
minItems: 1
|
|
336
|
+
items:
|
|
337
|
+
type: object
|
|
338
|
+
required: [id, title, type, priority]
|
|
339
|
+
additionalProperties: false
|
|
340
|
+
properties:
|
|
341
|
+
id: { type: string, pattern: '^[a-zA-Z0-9_\-]+$' }
|
|
342
|
+
title: { type: string, minLength: 3, maxLength: 200 }
|
|
343
|
+
type: { type: string, enum: [architecture, feature, bugfix] }
|
|
344
|
+
priority: { type: string, enum: [P0, P1, P2, P3] }
|
|
345
|
+
complexity: { type: integer, minimum: 1, maximum: 10 }
|
|
346
|
+
deps: { type: array, items: { type: string } }
|
|
347
|
+
files_modified: { type: array, items: { type: string } }
|
|
306
348
|
output_schema:
|
|
307
349
|
type: object
|
|
308
350
|
required: [parent_id, children]
|
|
@@ -313,22 +355,41 @@ tools:
|
|
|
313
355
|
errors:
|
|
314
356
|
- { code: TASK_NOT_FOUND, description: 'parent id missing' }
|
|
315
357
|
- { code: ALREADY_EXPANDED, description: 'task is already expanded' }
|
|
358
|
+
- { code: CHANGE_NOT_FOUND, description: 'the parent task change does not exist' }
|
|
359
|
+
- { code: CHANGE_NOT_MUTABLE, description: 'the parent task change no longer accepts children' }
|
|
360
|
+
- { code: BASELINE_NOT_READY, description: 'child creation requires a ready baseline or the parent task to belong to an already-authorized active change' }
|
|
361
|
+
- { code: INVALID_OUTPUT, description: 'the host-derived child task graph is invalid' }
|
|
316
362
|
cli_subcommand: task expand
|
|
317
363
|
emits_events: [task_expanded]
|
|
318
364
|
trace_to: '§7.3, D34'
|
|
319
365
|
|
|
320
366
|
- name: task.parse_prd
|
|
321
367
|
family: task
|
|
322
|
-
description:
|
|
368
|
+
description: Validate a host-derived PRD task graph and optionally persist it after the Ultra planning approval gate.
|
|
323
369
|
phase: '8a'
|
|
324
370
|
writer_role: mcp
|
|
325
371
|
input_schema:
|
|
326
372
|
type: object
|
|
373
|
+
required: [tasks]
|
|
327
374
|
additionalProperties: false
|
|
328
375
|
properties:
|
|
329
|
-
|
|
330
|
-
|
|
376
|
+
tasks:
|
|
377
|
+
type: array
|
|
378
|
+
minItems: 1
|
|
379
|
+
items:
|
|
380
|
+
type: object
|
|
381
|
+
required: [id, title, type, priority]
|
|
382
|
+
additionalProperties: false
|
|
383
|
+
properties:
|
|
384
|
+
id: { type: string, pattern: '^[a-zA-Z0-9_\-]+$' }
|
|
385
|
+
title: { type: string, minLength: 3, maxLength: 200 }
|
|
386
|
+
type: { type: string, enum: [architecture, feature, bugfix] }
|
|
387
|
+
priority: { type: string, enum: [P0, P1, P2, P3] }
|
|
388
|
+
complexity: { type: integer, minimum: 1, maximum: 10 }
|
|
389
|
+
deps: { type: array, items: { type: string } }
|
|
390
|
+
files_modified: { type: array, items: { type: string } }
|
|
331
391
|
tag: { type: string }
|
|
392
|
+
change_id: { type: string, pattern: '^[a-zA-Z0-9_\-]+$', description: 'continuous change unit that will own every persisted parsed task' }
|
|
332
393
|
dry_run: { type: boolean, default: false, description: 'when true, do not persist to state.db — used by ultra-plan gate to preview before approve' }
|
|
333
394
|
output_schema:
|
|
334
395
|
type: object
|
|
@@ -338,8 +399,11 @@ tools:
|
|
|
338
399
|
tasks: { type: array, items: { type: object } }
|
|
339
400
|
topo: { type: array, items: { type: array, items: { type: string } } }
|
|
340
401
|
errors:
|
|
341
|
-
- { code:
|
|
342
|
-
- { code:
|
|
402
|
+
- { code: PARSE_FAILED, description: 'the host-derived PRD task graph is invalid' }
|
|
403
|
+
- { code: CYCLE_DETECTED, description: 'the proposed task graph contains a dependency cycle' }
|
|
404
|
+
- { code: CHANGE_NOT_FOUND, description: 'assigned change does not exist' }
|
|
405
|
+
- { code: CHANGE_NOT_MUTABLE, description: 'assigned change no longer accepts tasks' }
|
|
406
|
+
- { code: BASELINE_NOT_READY, description: 'persisting parsed tasks requires a ready baseline or an already-authorized active change' }
|
|
343
407
|
cli_subcommand: task parse-prd
|
|
344
408
|
emits_events: [task_created]
|
|
345
409
|
trace_to: '§7.3, D34'
|
|
@@ -652,6 +716,15 @@ tools:
|
|
|
652
716
|
type: object
|
|
653
717
|
required: [project_name, mode]
|
|
654
718
|
additionalProperties: false
|
|
719
|
+
allOf:
|
|
720
|
+
- if:
|
|
721
|
+
required: [replace_ready]
|
|
722
|
+
properties: { replace_ready: { const: true } }
|
|
723
|
+
then: { required: [replacement_authorization] }
|
|
724
|
+
- if: { required: [replacement_authorization] }
|
|
725
|
+
then:
|
|
726
|
+
required: [replace_ready]
|
|
727
|
+
properties: { replace_ready: { const: true } }
|
|
655
728
|
properties:
|
|
656
729
|
id: { type: string, pattern: '^[a-zA-Z0-9_\-]+$', default: project-baseline }
|
|
657
730
|
project_name: { type: string, minLength: 1 }
|
|
@@ -661,6 +734,15 @@ tools:
|
|
|
661
734
|
scope: { type: array, minItems: 1, items: { type: string, minLength: 1 }, default: ['.'] }
|
|
662
735
|
repository_revision: { type: string, minLength: 1 }
|
|
663
736
|
replace_ready: { type: boolean, default: false }
|
|
737
|
+
replacement_authorization:
|
|
738
|
+
type: object
|
|
739
|
+
required: [approved_by, reason]
|
|
740
|
+
additionalProperties: false
|
|
741
|
+
properties:
|
|
742
|
+
approved_by: { type: string, minLength: 1 }
|
|
743
|
+
reason: { type: string, minLength: 3 }
|
|
744
|
+
replace_migrated: { type: boolean, default: false, description: explicitly supersede a compatibility-only migrated baseline and start evidence-backed adoption }
|
|
745
|
+
classification: { type: object, description: bounded repository classification evidence; content is limited to 32 KiB }
|
|
664
746
|
provider_refs:
|
|
665
747
|
type: object
|
|
666
748
|
additionalProperties: false
|
|
@@ -696,6 +778,9 @@ tools:
|
|
|
696
778
|
errors:
|
|
697
779
|
- { code: BASELINE_EXISTS, description: 'a ready baseline requires explicit replacement' }
|
|
698
780
|
- { code: BASELINE_IN_PROGRESS, description: 'another adoption is already active' }
|
|
781
|
+
- { code: BASELINE_REPLACEMENT_AUTHORIZATION_REQUIRED, description: 'ready-baseline replacement lacks durable owner approval and rationale' }
|
|
782
|
+
- { code: BASELINE_REPLACEMENT_INVALID, description: 'replacement flags do not match the current baseline lifecycle or authorization payload' }
|
|
783
|
+
- { code: BASELINE_SCOPE_MISSING, description: 'a selected baseline scope does not exist' }
|
|
699
784
|
- { code: PROVIDER_CONTENT_FORBIDDEN, description: 'provider payload content must remain external' }
|
|
700
785
|
cli_subcommand: baseline start
|
|
701
786
|
emits_events: [baseline_started]
|
|
@@ -754,6 +839,22 @@ tools:
|
|
|
754
839
|
summary: { type: string, minLength: 1 }
|
|
755
840
|
blocking: { type: boolean }
|
|
756
841
|
owner: { type: string, minLength: 1 }
|
|
842
|
+
gaps:
|
|
843
|
+
type: array
|
|
844
|
+
items:
|
|
845
|
+
type: object
|
|
846
|
+
required: [id, category, summary]
|
|
847
|
+
additionalProperties: false
|
|
848
|
+
properties:
|
|
849
|
+
id: { type: string, pattern: '^[a-zA-Z0-9_\-]+$' }
|
|
850
|
+
category: { type: string, enum: [baseline_blocker, documentation_drift, known_defect, technical_debt, unknown, future_change] }
|
|
851
|
+
status: { type: string, enum: [open, accepted, resolved, deferred], default: open }
|
|
852
|
+
blocking: { type: boolean, default: false }
|
|
853
|
+
summary: { type: string, minLength: 1 }
|
|
854
|
+
evidence_refs: { type: array, uniqueItems: true, items: { type: string, minLength: 1 }, default: [] }
|
|
855
|
+
owner: { type: ['string', 'null'], minLength: 1 }
|
|
856
|
+
resolution: { type: string, minLength: 1 }
|
|
857
|
+
classification: { type: object, description: bounded repository classification evidence; content is limited to 32 KiB }
|
|
757
858
|
provider_refs:
|
|
758
859
|
type: object
|
|
759
860
|
additionalProperties: false
|
|
@@ -790,6 +891,7 @@ tools:
|
|
|
790
891
|
- { code: BASELINE_NOT_FOUND, description: 'baseline id does not exist' }
|
|
791
892
|
- { code: BASELINE_NOT_MUTABLE, description: 'ready and superseded baselines are immutable' }
|
|
792
893
|
- { code: BASELINE_SPEC_MISSING, description: 'a declared specification file does not exist' }
|
|
894
|
+
- { code: BASELINE_SCOPE_MISSING, description: 'a selected baseline scope does not exist' }
|
|
793
895
|
- { code: PROVIDER_CONTENT_FORBIDDEN, description: 'provider payload content must remain external' }
|
|
794
896
|
cli_subcommand: baseline record
|
|
795
897
|
emits_events: [baseline_recorded]
|
|
@@ -831,6 +933,7 @@ tools:
|
|
|
831
933
|
approved_by: { type: string, minLength: 1 }
|
|
832
934
|
approval_note: { type: string, minLength: 3 }
|
|
833
935
|
accept_known_red: { type: boolean, default: false }
|
|
936
|
+
accept_dirty_worktree: { type: boolean, default: false, description: explicitly freeze the exact recorded dirty-worktree digest into the baseline }
|
|
834
937
|
output_schema:
|
|
835
938
|
type: object
|
|
836
939
|
required: [ready, status, blockers, baseline]
|
|
@@ -843,6 +946,7 @@ tools:
|
|
|
843
946
|
errors:
|
|
844
947
|
- { code: BASELINE_NOT_FOUND, description: 'baseline id does not exist' }
|
|
845
948
|
- { code: BASELINE_NOT_CONVERGEABLE, description: 'baseline lifecycle does not allow convergence' }
|
|
949
|
+
- { code: BASELINE_NOT_READY, description: 'baseline evidence, gaps, revision, branch, worktree, or verification still blocks convergence' }
|
|
846
950
|
cli_subcommand: baseline converge
|
|
847
951
|
emits_events: [baseline_blocked, baseline_converged]
|
|
848
952
|
|
|
@@ -862,6 +966,14 @@ tools:
|
|
|
862
966
|
kind: { type: string, enum: [quick, standard, major, incident] }
|
|
863
967
|
intent: { type: string, minLength: 3 }
|
|
864
968
|
base_commit: { type: string, pattern: '^[0-9a-f]{7,40}$' }
|
|
969
|
+
baseline_bypass:
|
|
970
|
+
type: object
|
|
971
|
+
required: [reason, approved_by]
|
|
972
|
+
additionalProperties: false
|
|
973
|
+
properties:
|
|
974
|
+
reason: { type: string, minLength: 3 }
|
|
975
|
+
approved_by: { type: string, minLength: 1 }
|
|
976
|
+
description: incident-only break-glass approval when the project baseline is unhealthy
|
|
865
977
|
docs_impact:
|
|
866
978
|
type: object
|
|
867
979
|
additionalProperties: false
|
|
@@ -907,6 +1019,8 @@ tools:
|
|
|
907
1019
|
- { code: DUPLICATE_CHANGE_ID, description: 'change id already exists' }
|
|
908
1020
|
- { code: PROVIDER_CONTENT_FORBIDDEN, description: 'provider payload content must remain external' }
|
|
909
1021
|
- { code: VALIDATION_ERROR, description: 'change input failed validation' }
|
|
1022
|
+
- { code: BASELINE_NOT_READY, description: 'ordinary work is blocked until the project baseline is healthy' }
|
|
1023
|
+
- { code: BASELINE_BYPASS_REQUIRED, description: 'an incident on an unhealthy baseline requires explicit break-glass approval' }
|
|
910
1024
|
cli_subcommand: change create
|
|
911
1025
|
emits_events: [change_created, context_updated]
|
|
912
1026
|
|
|
@@ -27,10 +27,18 @@ CREATE TABLE IF NOT EXISTS baselines (
|
|
|
27
27
|
repository_root TEXT NOT NULL DEFAULT '.',
|
|
28
28
|
scope_json TEXT NOT NULL DEFAULT '["."]',
|
|
29
29
|
repository_revision TEXT,
|
|
30
|
+
repository_branch TEXT,
|
|
31
|
+
worktree_state TEXT NOT NULL DEFAULT 'unavailable'
|
|
32
|
+
CHECK (worktree_state IN ('clean', 'dirty', 'unavailable')),
|
|
33
|
+
worktree_digest TEXT,
|
|
34
|
+
worktree_files_json TEXT NOT NULL DEFAULT '[]',
|
|
35
|
+
worktree_accepted INTEGER NOT NULL DEFAULT 0 CHECK (worktree_accepted IN (0, 1)),
|
|
30
36
|
spec_refs_json TEXT NOT NULL DEFAULT '[]',
|
|
31
37
|
evidence_json TEXT NOT NULL DEFAULT '[]',
|
|
32
38
|
verification_json TEXT NOT NULL DEFAULT '[]',
|
|
33
39
|
unknowns_json TEXT NOT NULL DEFAULT '[]',
|
|
40
|
+
gaps_json TEXT NOT NULL DEFAULT '[]',
|
|
41
|
+
classification_json TEXT NOT NULL DEFAULT '{}',
|
|
34
42
|
provider_refs_json TEXT NOT NULL DEFAULT '{}',
|
|
35
43
|
approved_by TEXT,
|
|
36
44
|
approval_note TEXT,
|
|
@@ -53,6 +61,7 @@ CREATE TABLE IF NOT EXISTS changes (
|
|
|
53
61
|
intent TEXT NOT NULL,
|
|
54
62
|
docs_impact_json TEXT NOT NULL DEFAULT '{"status":"unknown","files":[],"rationale":null}',
|
|
55
63
|
provider_refs_json TEXT NOT NULL DEFAULT '{}',
|
|
64
|
+
baseline_bypass_json TEXT,
|
|
56
65
|
base_commit TEXT,
|
|
57
66
|
artifact_root TEXT NOT NULL,
|
|
58
67
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
|
|
@@ -336,3 +345,5 @@ INSERT OR IGNORE INTO schema_version (version, description)
|
|
|
336
345
|
VALUES ('10.0', 'Role-scoped context snapshots, deterministic breadcrumbs, and approval-gated specification learning');
|
|
337
346
|
INSERT OR IGNORE INTO schema_version (version, description)
|
|
338
347
|
VALUES ('11.0', 'Greenfield and brownfield baseline adoption, convergence, and drift authority');
|
|
348
|
+
INSERT OR IGNORE INTO schema_version (version, description)
|
|
349
|
+
VALUES ('12.0', 'Evidence-backed repository snapshots, gap ledger, safe re-adoption, and incident break-glass governance');
|
package/ultra-tools/cli.cjs
CHANGED
|
@@ -20,7 +20,7 @@ const path = require('node:path');
|
|
|
20
20
|
|
|
21
21
|
const VERSION = (() => {
|
|
22
22
|
try {
|
|
23
|
-
const pkg = require(
|
|
23
|
+
const pkg = require('../package.json');
|
|
24
24
|
return pkg.version;
|
|
25
25
|
} catch (_err) {
|
|
26
26
|
return '0.1.0';
|
|
@@ -35,10 +35,10 @@ USAGE:
|
|
|
35
35
|
SUBCOMMANDS:
|
|
36
36
|
task init-project
|
|
37
37
|
session close | get | list | admission | heartbeat | subscribe | reap
|
|
38
|
-
system doctor [--repair]
|
|
38
|
+
system doctor [--repair] | restore | rebaseline
|
|
39
39
|
status [--cost] [--since <duration>] [--json]
|
|
40
40
|
db init | checkpoint | vacuum | integrity | backup (Phase 2)
|
|
41
|
-
migrate --from
|
|
41
|
+
migrate --from=<4.4|4.5> --to=<4.5|12.0> [--dry|--rollback]
|
|
42
42
|
legacy-memory inspect | archive | prune --confirm DELETE_ULTRA_LEGACY_MEMORY
|
|
43
43
|
|
|
44
44
|
--help / -h show this message
|
|
@@ -68,7 +68,11 @@ const statusCommand = require('./commands/status.cjs');
|
|
|
68
68
|
const legacyMemoryCommand = require('./commands/legacy-memory.cjs');
|
|
69
69
|
const systemCommand = require('./commands/system.cjs');
|
|
70
70
|
|
|
71
|
-
//
|
|
71
|
+
// Session commands run only against healthy authority and may emit best-effort
|
|
72
|
+
// telemetry. Initialization, diagnosis, migration, and recovery commands must
|
|
73
|
+
// never open state.db before their own evidence-preserving checks.
|
|
74
|
+
const TELEMETRY_SUBCOMMANDS = new Set(['session']);
|
|
75
|
+
|
|
72
76
|
function emitCliTelemetry(sub, rest) {
|
|
73
77
|
try {
|
|
74
78
|
const dbPath = path.resolve('.ultra', 'state.db');
|
|
@@ -112,7 +116,7 @@ function main(argv) {
|
|
|
112
116
|
const [sub, ...rest] = args;
|
|
113
117
|
const handler = SUBCOMMANDS[sub];
|
|
114
118
|
if (!handler) fail(`unknown subcommand: ${sub}\n\n${USAGE}`);
|
|
115
|
-
emitCliTelemetry(sub, rest);
|
|
119
|
+
if (TELEMETRY_SUBCOMMANDS.has(sub)) emitCliTelemetry(sub, rest);
|
|
116
120
|
handler(rest);
|
|
117
121
|
}
|
|
118
122
|
|
|
@@ -3,12 +3,17 @@
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
|
|
6
|
-
const {
|
|
6
|
+
const {
|
|
7
|
+
initStateDb, openStateDb, closeStateDb, MIGRATED_GAPS,
|
|
8
|
+
} = require('../../mcp-server/lib/state-db.cjs');
|
|
7
9
|
const ops = require('../../mcp-server/lib/state-ops.cjs');
|
|
8
10
|
const projector = require('../../mcp-server/lib/projector.cjs');
|
|
9
11
|
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
+
const DEFAULT_FROM = '4.4';
|
|
13
|
+
const SUPPORTED_TRANSITIONS = Object.freeze({
|
|
14
|
+
'4.4': '4.5',
|
|
15
|
+
'4.5': '12.0',
|
|
16
|
+
});
|
|
12
17
|
|
|
13
18
|
// Frozen SQL — values flow through parameter bindings.
|
|
14
19
|
const RECORD_MIGRATION_SQL = "INSERT INTO migration_history (from_version, to_version, direction, status, notes) VALUES (@from, @to, @direction, @status, @notes)";
|
|
@@ -73,7 +78,7 @@ function copyDirSync(src, dst) {
|
|
|
73
78
|
const s = path.join(src, entry.name);
|
|
74
79
|
const d = path.join(dst, entry.name);
|
|
75
80
|
if (entry.name === 'state.db' || entry.name === 'state.db-wal' || entry.name === 'state.db-shm') continue;
|
|
76
|
-
if (entry.name
|
|
81
|
+
if (/^backup-v[^/]+-/.test(entry.name)) continue;
|
|
77
82
|
if (entry.isDirectory()) copyDirSync(s, d);
|
|
78
83
|
else fs.copyFileSync(s, d);
|
|
79
84
|
}
|
|
@@ -148,15 +153,15 @@ function normalizeLegacyTask(task, tasksJson, contextHeaders, sourceDir) {
|
|
|
148
153
|
};
|
|
149
154
|
}
|
|
150
155
|
|
|
151
|
-
function planForward(sourceDir) {
|
|
156
|
+
function planForward(sourceDir, expectedFrom = DEFAULT_FROM) {
|
|
152
157
|
const tasksPath = path.join(sourceDir, '.ultra', 'tasks', 'tasks.json');
|
|
153
158
|
const tasksJson = readJsonOptional(tasksPath);
|
|
154
159
|
if (!tasksJson || !Array.isArray(tasksJson.tasks)) {
|
|
155
160
|
throw new Error(`migrate: tasks.json missing or malformed at ${tasksPath}`);
|
|
156
161
|
}
|
|
157
162
|
const version = String(tasksJson.version || tasksJson.schema_version || '');
|
|
158
|
-
if (version !==
|
|
159
|
-
throw new Error(`migrate: expected v${
|
|
163
|
+
if (version !== expectedFrom) {
|
|
164
|
+
throw new Error(`migrate: expected v${expectedFrom} tasks.json, found ${version || '(missing version)'}`);
|
|
160
165
|
}
|
|
161
166
|
|
|
162
167
|
const contextHeaders = {};
|
|
@@ -173,7 +178,7 @@ function planForward(sourceDir) {
|
|
|
173
178
|
task_id: task.id,
|
|
174
179
|
json_status: task.status,
|
|
175
180
|
context_status: ctx.status,
|
|
176
|
-
resolution:
|
|
181
|
+
resolution: `tasks.json wins (v${expectedFrom} projection migration rule)`,
|
|
177
182
|
});
|
|
178
183
|
}
|
|
179
184
|
}
|
|
@@ -190,6 +195,7 @@ function planForward(sourceDir) {
|
|
|
190
195
|
events: eventList,
|
|
191
196
|
contextHeaders,
|
|
192
197
|
warnings,
|
|
198
|
+
projectName: tasksJson.project?.name || path.basename(sourceDir),
|
|
193
199
|
};
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -241,16 +247,17 @@ function recordMigration(db, { from, to, direction, status, notes }) {
|
|
|
241
247
|
db.prepare(RECORD_MIGRATION_SQL).run({ from, to, direction, status, notes });
|
|
242
248
|
}
|
|
243
249
|
|
|
244
|
-
function ensureBackupName(sourceDir) {
|
|
250
|
+
function ensureBackupName(sourceDir, fromVersion = DEFAULT_FROM) {
|
|
245
251
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
246
|
-
return path.join(sourceDir, '.ultra', `backup-
|
|
252
|
+
return path.join(sourceDir, '.ultra', `backup-v${fromVersion}-${ts}`);
|
|
247
253
|
}
|
|
248
254
|
|
|
249
|
-
function findLatestBackup(sourceDir) {
|
|
255
|
+
function findLatestBackup(sourceDir, fromVersion = DEFAULT_FROM) {
|
|
250
256
|
const ultra = path.join(sourceDir, '.ultra');
|
|
251
257
|
if (!fs.existsSync(ultra)) return null;
|
|
258
|
+
const prefix = `backup-v${fromVersion}-`;
|
|
252
259
|
const candidates = fs.readdirSync(ultra)
|
|
253
|
-
.filter((n) => n.startsWith(
|
|
260
|
+
.filter((n) => n.startsWith(prefix))
|
|
254
261
|
.sort();
|
|
255
262
|
return candidates.length === 0 ? null : path.join(ultra, candidates[candidates.length - 1]);
|
|
256
263
|
}
|
|
@@ -258,9 +265,11 @@ function findLatestBackup(sourceDir) {
|
|
|
258
265
|
function cmdForward(flags) {
|
|
259
266
|
const sourceDir = path.resolve(flags.sourceDir || '.');
|
|
260
267
|
const dbPath = path.resolve(flags.dbPath || path.join(sourceDir, '.ultra', 'state.db'));
|
|
268
|
+
const fromVersion = flags.from || DEFAULT_FROM;
|
|
269
|
+
const toVersion = flags.to || SUPPORTED_TRANSITIONS[fromVersion];
|
|
261
270
|
let plan;
|
|
262
271
|
try {
|
|
263
|
-
plan = planForward(sourceDir);
|
|
272
|
+
plan = planForward(sourceDir, fromVersion);
|
|
264
273
|
} catch (err) {
|
|
265
274
|
emit({ ok: false, error: { code: 'MIGRATE_FAILED', message: err.message, retriable: false } });
|
|
266
275
|
return 2;
|
|
@@ -271,8 +280,8 @@ function cmdForward(flags) {
|
|
|
271
280
|
ok: true,
|
|
272
281
|
data: {
|
|
273
282
|
mode: 'dry',
|
|
274
|
-
from:
|
|
275
|
-
to:
|
|
283
|
+
from: fromVersion,
|
|
284
|
+
to: toVersion,
|
|
276
285
|
source_dir: sourceDir,
|
|
277
286
|
db_path: dbPath,
|
|
278
287
|
tasks_to_insert: plan.tasks.length,
|
|
@@ -284,7 +293,7 @@ function cmdForward(flags) {
|
|
|
284
293
|
}
|
|
285
294
|
|
|
286
295
|
// Real run: backup the entire .ultra subtree first.
|
|
287
|
-
const backupDir = ensureBackupName(sourceDir);
|
|
296
|
+
const backupDir = ensureBackupName(sourceDir, fromVersion);
|
|
288
297
|
const ultraSource = path.join(sourceDir, '.ultra');
|
|
289
298
|
copyDirSync(ultraSource, backupDir);
|
|
290
299
|
|
|
@@ -297,9 +306,18 @@ function cmdForward(flags) {
|
|
|
297
306
|
throw new Error(`migrate: refusing to merge into non-empty state.db (tasks=${existing})`);
|
|
298
307
|
}
|
|
299
308
|
const inserted = applyForward(db, plan);
|
|
309
|
+
db.prepare(
|
|
310
|
+
`INSERT INTO baselines
|
|
311
|
+
(id, project_name, mode, status, gaps_json, approval_note)
|
|
312
|
+
VALUES ('migrated-baseline', ?, 'migrated', 'adopting', ?, ?)`,
|
|
313
|
+
).run(
|
|
314
|
+
plan.projectName,
|
|
315
|
+
JSON.stringify(MIGRATED_GAPS),
|
|
316
|
+
`Legacy v${fromVersion} projection imported; evidence-backed owner re-adoption is required`,
|
|
317
|
+
);
|
|
300
318
|
recordMigration(db, {
|
|
301
|
-
from:
|
|
302
|
-
to:
|
|
319
|
+
from: fromVersion,
|
|
320
|
+
to: toVersion,
|
|
303
321
|
direction: 'forward',
|
|
304
322
|
status: 'success',
|
|
305
323
|
notes: `tasks=${inserted.taskInserted} events=${inserted.eventsInserted} warnings=${plan.warnings.length}`,
|
|
@@ -312,8 +330,8 @@ function cmdForward(flags) {
|
|
|
312
330
|
ok: true,
|
|
313
331
|
data: {
|
|
314
332
|
mode: 'apply',
|
|
315
|
-
from:
|
|
316
|
-
to:
|
|
333
|
+
from: fromVersion,
|
|
334
|
+
to: toVersion,
|
|
317
335
|
source_dir: sourceDir,
|
|
318
336
|
db_path: dbPath,
|
|
319
337
|
backup_dir: backupDir,
|
|
@@ -328,8 +346,8 @@ function cmdForward(flags) {
|
|
|
328
346
|
if (db) {
|
|
329
347
|
try {
|
|
330
348
|
recordMigration(db, {
|
|
331
|
-
from:
|
|
332
|
-
to:
|
|
349
|
+
from: fromVersion,
|
|
350
|
+
to: toVersion,
|
|
333
351
|
direction: 'forward',
|
|
334
352
|
status: 'failed',
|
|
335
353
|
notes: err.message,
|
|
@@ -346,9 +364,11 @@ function cmdForward(flags) {
|
|
|
346
364
|
function cmdRollback(flags) {
|
|
347
365
|
const sourceDir = path.resolve(flags.sourceDir || '.');
|
|
348
366
|
const dbPath = path.resolve(flags.dbPath || path.join(sourceDir, '.ultra', 'state.db'));
|
|
349
|
-
const
|
|
367
|
+
const fromVersion = flags.from || DEFAULT_FROM;
|
|
368
|
+
const toVersion = flags.to || SUPPORTED_TRANSITIONS[fromVersion];
|
|
369
|
+
const backupDir = findLatestBackup(sourceDir, fromVersion);
|
|
350
370
|
if (!backupDir) {
|
|
351
|
-
emit({ ok: false, error: { code: 'NO_BACKUP', message:
|
|
371
|
+
emit({ ok: false, error: { code: 'NO_BACKUP', message: `no backup-v${fromVersion}-* directory found` } });
|
|
352
372
|
return 2;
|
|
353
373
|
}
|
|
354
374
|
|
|
@@ -358,8 +378,8 @@ function cmdRollback(flags) {
|
|
|
358
378
|
if (fs.existsSync(dbPath)) {
|
|
359
379
|
db = openStateDb(dbPath);
|
|
360
380
|
recordMigration(db, {
|
|
361
|
-
from:
|
|
362
|
-
to:
|
|
381
|
+
from: toVersion,
|
|
382
|
+
to: fromVersion,
|
|
363
383
|
direction: 'rollback',
|
|
364
384
|
status: 'success',
|
|
365
385
|
notes: `restored from ${backupDir}`,
|
|
@@ -376,7 +396,7 @@ function cmdRollback(flags) {
|
|
|
376
396
|
// Restore .ultra contents from backup, leaving the backup itself in place.
|
|
377
397
|
const ultraDir = path.join(sourceDir, '.ultra');
|
|
378
398
|
for (const entry of fs.readdirSync(ultraDir, { withFileTypes: true })) {
|
|
379
|
-
if (entry.name
|
|
399
|
+
if (/^backup-v[^/]+-/.test(entry.name)) continue;
|
|
380
400
|
const target = path.join(ultraDir, entry.name);
|
|
381
401
|
fs.rmSync(target, { recursive: true, force: true });
|
|
382
402
|
}
|
|
@@ -386,8 +406,8 @@ function cmdRollback(flags) {
|
|
|
386
406
|
ok: true,
|
|
387
407
|
data: {
|
|
388
408
|
mode: 'rollback',
|
|
389
|
-
from:
|
|
390
|
-
to:
|
|
409
|
+
from: toVersion,
|
|
410
|
+
to: fromVersion,
|
|
391
411
|
backup_dir: backupDir,
|
|
392
412
|
source_dir: sourceDir,
|
|
393
413
|
},
|
|
@@ -406,29 +426,35 @@ function dispatch(args) {
|
|
|
406
426
|
process.stdout.write(USAGE);
|
|
407
427
|
return 0;
|
|
408
428
|
}
|
|
409
|
-
|
|
410
|
-
|
|
429
|
+
const fromVersion = flags.from || DEFAULT_FROM;
|
|
430
|
+
const expectedTo = SUPPORTED_TRANSITIONS[fromVersion];
|
|
431
|
+
if (!expectedTo) {
|
|
432
|
+
emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--from ${fromVersion} unsupported (supported: ${Object.keys(SUPPORTED_TRANSITIONS).join(', ')})` } });
|
|
411
433
|
return 1;
|
|
412
434
|
}
|
|
413
|
-
if (flags.to && flags.to !==
|
|
414
|
-
emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--to ${flags.to} unsupported (
|
|
435
|
+
if (flags.to && flags.to !== expectedTo) {
|
|
436
|
+
emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--to ${flags.to} unsupported for --from ${fromVersion} (expected ${expectedTo})` } });
|
|
415
437
|
return 1;
|
|
416
438
|
}
|
|
417
439
|
return flags.rollback ? cmdRollback(flags) : cmdForward(flags);
|
|
418
440
|
}
|
|
419
441
|
|
|
420
|
-
const USAGE = `ultra-tools migrate --from
|
|
442
|
+
const USAGE = `ultra-tools migrate --from=<version> --to=<version> [flags]
|
|
443
|
+
|
|
444
|
+
Supported transitions:
|
|
445
|
+
4.4 -> 4.5 import the legacy task projection into authoritative state
|
|
446
|
+
4.5 -> 12.0 import a projection-only project into current authoritative state
|
|
421
447
|
|
|
422
448
|
Flags:
|
|
423
449
|
--source-dir <dir> project root containing .ultra/ (default: .)
|
|
424
450
|
--db-path <path> state.db destination (default: <source-dir>/.ultra/state.db)
|
|
425
451
|
--dry print the migration plan without writing
|
|
426
|
-
--rollback restore the most recent backup-
|
|
452
|
+
--rollback restore the most recent matching backup-v<from>-* and drop state.db
|
|
427
453
|
|
|
428
454
|
The forward flow: backup .ultra/ → init state.db → insert tasks from
|
|
429
455
|
tasks.json → merge context md status (tasks.json wins on conflict, warnings
|
|
430
456
|
recorded) → insert activity-log events → record migration_history.
|
|
431
|
-
Rollback restores from the latest backup-
|
|
457
|
+
Rollback restores from the latest matching backup-v<from>-* directory and writes a
|
|
432
458
|
matching migration_history rollback row before dropping state.db.
|
|
433
459
|
`;
|
|
434
460
|
|