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.
Files changed (88) 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 +95 -1
  4. package/README.md +31 -16
  5. package/adapters/_shared/codex-assets.cjs +23 -163
  6. package/adapters/_shared/handbook.cjs +2 -2
  7. package/adapters/_shared/runtime-assets.cjs +1 -0
  8. package/adapters/_shared/tests/runtime-assets.test.cjs +18 -0
  9. package/adapters/opencode.js +30 -120
  10. package/adapters/tests/claude.test.cjs +3 -2
  11. package/adapters/tests/codex-hook.test.cjs +34 -7
  12. package/adapters/tests/codex.test.cjs +21 -6
  13. package/adapters/tests/kimi-hook.test.cjs +34 -7
  14. package/adapters/tests/opencode.test.cjs +70 -84
  15. package/commands/ultra-init.md +1 -1
  16. package/docs/AGENT-CONTEXT.md +10 -6
  17. package/docs/ARCHITECTURE.md +40 -17
  18. package/docs/PLAN.zh-CN.md +5 -4
  19. package/docs/ROADMAP.md +2 -2
  20. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  21. package/docs/STATE-DB-ACCESS-POLICY.md +3 -3
  22. package/docs/USER-HANDBOOK-CONTRACT.md +5 -4
  23. package/hooks/active_task_context.py +22 -50
  24. package/hooks/context_spine.py +61 -299
  25. package/hooks/health_check.py +19 -17
  26. package/hooks/pre_stop_check.py +28 -33
  27. package/hooks/subagent_tracker.py +13 -19
  28. package/hooks/workflow_checkpoint.py +39 -47
  29. package/hooks/workflow_context.py +4 -9
  30. package/hooks/workflow_resume.py +48 -128
  31. package/mcp-server/breadcrumb.cjs +22 -0
  32. package/mcp-server/lib/archive-journal.cjs +192 -0
  33. package/mcp-server/lib/archive-journal.test.cjs +35 -0
  34. package/mcp-server/lib/baseline-workflow.cjs +404 -38
  35. package/mcp-server/lib/baseline-workflow.test.cjs +252 -0
  36. package/mcp-server/lib/change-workflow.cjs +249 -75
  37. package/mcp-server/lib/change-workflow.test.cjs +235 -1
  38. package/mcp-server/lib/context-spine.test.cjs +5 -0
  39. package/mcp-server/lib/doctor.cjs +14 -1
  40. package/mcp-server/lib/doctor.test.cjs +42 -4
  41. package/mcp-server/lib/init-project.cjs +292 -9
  42. package/mcp-server/lib/prd-parser.cjs +14 -41
  43. package/mcp-server/lib/prd-parser.test.cjs +49 -138
  44. package/mcp-server/lib/project-breadcrumb.cjs +139 -0
  45. package/mcp-server/lib/project-breadcrumb.test.cjs +112 -0
  46. package/mcp-server/lib/spec-learning.test.cjs +1 -1
  47. package/mcp-server/lib/state-authority.cjs +9 -3
  48. package/mcp-server/lib/state-db.cjs +154 -32
  49. package/mcp-server/lib/state-db.test.cjs +136 -4
  50. package/mcp-server/lib/state-ops.cjs +68 -6
  51. package/mcp-server/lib/task-expander.cjs +14 -37
  52. package/mcp-server/lib/task-expander.test.cjs +80 -164
  53. package/mcp-server/server.cjs +34 -37
  54. package/mcp-server/tests/init-project.test.cjs +162 -1
  55. package/mcp-server/tests/migrate.test.cjs +33 -0
  56. package/mcp-server/tests/server-contract.test.cjs +391 -17
  57. package/mcp-server/tests/state-db.test.cjs +14 -2
  58. package/mcp-server/tests/state-ops.test.cjs +108 -0
  59. package/package.json +2 -4
  60. package/skills/cc-collab/SKILL.md +2 -2
  61. package/skills/codex-collab/SKILL.md +2 -3
  62. package/skills/ultra-change/SKILL.md +18 -10
  63. package/skills/ultra-deliver/SKILL.md +13 -9
  64. package/skills/ultra-dev/SKILL.md +6 -4
  65. package/skills/ultra-doctor/SKILL.md +22 -7
  66. package/skills/ultra-init/SKILL.md +104 -59
  67. package/skills/ultra-init/references/brownfield-adoption.md +65 -37
  68. package/skills/ultra-plan/SKILL.md +20 -7
  69. package/skills/ultra-research/SKILL.md +12 -5
  70. package/skills/ultra-status/SKILL.md +6 -4
  71. package/skills/ultra-verify/SKILL.md +21 -30
  72. package/skills/ultra-verify/scripts/verify_wait.py +54 -54
  73. package/spec/cli-protocol.md +11 -1
  74. package/spec/mcp-tools.yaml +133 -19
  75. package/spec/schemas/state-db.sql +11 -0
  76. package/ultra-tools/cli.cjs +9 -5
  77. package/ultra-tools/commands/migrate.cjs +62 -36
  78. package/ultra-tools/commands/status.cjs +18 -9
  79. package/ultra-tools/commands/system.cjs +340 -6
  80. package/ultra-tools/commands/task.cjs +7 -1
  81. package/ultra-tools/status-command.test.cjs +29 -0
  82. package/ultra-tools/system-command.test.cjs +252 -1
  83. package/mcp-server/lib/llm-client.cjs +0 -157
  84. package/mcp-server/lib/llm-client.test.cjs +0 -156
  85. package/orchestrator/planner/expand-prompt.cjs +0 -62
  86. package/orchestrator/planner/expand-prompt.test.cjs +0 -52
  87. package/orchestrator/planner/prd-prompt.cjs +0 -44
  88. package/orchestrator/planner/prd-prompt.test.cjs +0 -30
@@ -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.1'
6
+ version: '1.2'
7
7
 
8
8
  info:
9
9
  title: Ultra Builder Pro MCP tool contracts
10
- phase: '1.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 for task.expand children' }
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: Expand a parent task into N child sub-tasks (Phase 8A).
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: { type: string }
304
- sub_count: { type: integer, minimum: 1, maximum: 16 }
305
- strategy: { type: string, enum: [llm], default: llm }
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: Parse a PRD document into a task graph (Phase 8A). When dry_run=true the tasks are returned without writing to state.db (supports ultra-plan human gate workflow).
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
- prd_path: { type: string }
330
- prd_text: { type: string }
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: NO_INPUT, description: 'one of prd_path or prd_text required' }
342
- - { code: PARSE_FAILED, description: 'PRD could not be parsed into tasks' }
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');
@@ -20,7 +20,7 @@ const path = require('node:path');
20
20
 
21
21
  const VERSION = (() => {
22
22
  try {
23
- const pkg = require(path.join(__dirname, '..', 'package.json'));
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=4.4 --to=4.5 [--dry|--rollback] (Phase 2)
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
- // Phase 6.2 — CLI telemetry: best-effort, never blocks the subcommand.
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 { initStateDb, openStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
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 SUPPORTED_FROM = '4.4';
11
- const SUPPORTED_TO = '4.5';
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.startsWith('backup-v4.4-')) continue;
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 !== SUPPORTED_FROM) {
159
- throw new Error(`migrate: expected v${SUPPORTED_FROM} tasks.json, found ${version || '(missing version)'}`);
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: 'tasks.json wins (v4.4 → v4.5 D21 rule)',
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-v4.4-${ts}`);
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('backup-v4.4-'))
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: SUPPORTED_FROM,
275
- to: SUPPORTED_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: SUPPORTED_FROM,
302
- to: SUPPORTED_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: SUPPORTED_FROM,
316
- to: SUPPORTED_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: SUPPORTED_FROM,
332
- to: SUPPORTED_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 backupDir = findLatestBackup(sourceDir);
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: 'no backup-v4.4-* directory found' } });
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: SUPPORTED_TO,
362
- to: SUPPORTED_FROM,
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.startsWith('backup-v4.4-')) continue;
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: SUPPORTED_TO,
390
- to: SUPPORTED_FROM,
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
- if (flags.from && flags.from !== SUPPORTED_FROM) {
410
- emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--from ${flags.from} unsupported (only ${SUPPORTED_FROM})` } });
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 !== SUPPORTED_TO) {
414
- emit({ ok: false, error: { code: 'UNSUPPORTED_VERSION', message: `--to ${flags.to} unsupported (only ${SUPPORTED_TO})` } });
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=4.4 --to=4.5 [flags]
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-v4.4-* and drop state.db
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-v4.4-* directory and writes a
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