ultra-builder-pro-cli 0.6.0 → 0.7.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 (68) hide show
  1. package/.ultra-template/changes/active/.gitkeep +1 -0
  2. package/.ultra-template/changes/archive/.gitkeep +1 -0
  3. package/CHANGELOG.md +62 -11
  4. package/README.md +26 -18
  5. package/adapters/_shared/handbook.cjs +7 -7
  6. package/adapters/_shared/runtime-assets.cjs +2 -0
  7. package/adapters/_shared/tests/handbook.test.cjs +4 -1
  8. package/adapters/_shared/tests/runtime-assets.test.cjs +2 -0
  9. package/adapters/opencode.js +81 -20
  10. package/adapters/tests/codex.test.cjs +5 -3
  11. package/adapters/tests/opencode.test.cjs +16 -9
  12. package/bin/install.js +0 -19
  13. package/bin/orchestrator.js +1 -26
  14. package/commands/ultra-change.md +47 -0
  15. package/commands/ultra-deliver.md +14 -7
  16. package/commands/ultra-dev.md +6 -3
  17. package/commands/ultra-doctor.md +36 -0
  18. package/commands/ultra-init.md +4 -3
  19. package/commands/ultra-status.md +7 -5
  20. package/commands/ultra-test.md +2 -1
  21. package/docs/AGENT-CONTEXT.md +31 -23
  22. package/docs/ARCHITECTURE.md +46 -17
  23. package/docs/PLAN.zh-CN.md +35 -29
  24. package/docs/ROADMAP.md +14 -14
  25. package/docs/RUNTIME-COMPAT-MATRIX.md +11 -11
  26. package/hooks/README.md +8 -7
  27. package/hooks/active_task_context.py +22 -15
  28. package/hooks/health_check.py +136 -38
  29. package/hooks/workflow_context.py +120 -30
  30. package/mcp-server/lib/change-workflow.cjs +556 -0
  31. package/mcp-server/lib/change-workflow.test.cjs +259 -0
  32. package/mcp-server/lib/doctor.cjs +112 -0
  33. package/mcp-server/lib/doctor.test.cjs +86 -0
  34. package/mcp-server/lib/init-project.cjs +32 -6
  35. package/mcp-server/lib/runtime-state.cjs +222 -0
  36. package/mcp-server/lib/runtime-state.test.cjs +92 -0
  37. package/mcp-server/lib/state-db.cjs +31 -8
  38. package/mcp-server/lib/state-ops.cjs +17 -3
  39. package/mcp-server/server.cjs +102 -10
  40. package/mcp-server/tests/db-ops.test.cjs +5 -1
  41. package/mcp-server/tests/init-project.test.cjs +45 -0
  42. package/mcp-server/tests/server-contract.test.cjs +123 -1
  43. package/mcp-server/tests/state-db.test.cjs +44 -7
  44. package/mcp-server/tests/state-ops.test.cjs +19 -0
  45. package/orchestrator/daemon.cjs +39 -0
  46. package/orchestrator/tests/daemon.test.cjs +50 -0
  47. package/package.json +3 -3
  48. package/settings.json +1 -2
  49. package/skills/ultra-change/SKILL.md +140 -0
  50. package/skills/ultra-deliver/SKILL.md +81 -15
  51. package/skills/ultra-dev/SKILL.md +32 -12
  52. package/skills/ultra-doctor/SKILL.md +90 -0
  53. package/skills/ultra-init/SKILL.md +8 -5
  54. package/skills/ultra-review/SKILL.md +16 -0
  55. package/skills/ultra-status/SKILL.md +30 -7
  56. package/skills/ultra-test/SKILL.md +14 -2
  57. package/spec/cli-protocol.md +12 -1
  58. package/spec/mcp-tools.yaml +337 -1
  59. package/spec/schemas/mcp-tools.schema.json +1 -1
  60. package/spec/schemas/state-db.sql +117 -0
  61. package/ultra-tools/cli.cjs +3 -0
  62. package/ultra-tools/commands/system.cjs +57 -0
  63. package/ultra-tools/system-command.test.cjs +53 -0
  64. package/adapters/_shared/rtk-detect.cjs +0 -86
  65. package/adapters/_shared/tests/rtk-detect.test.cjs +0 -116
  66. package/docs/LEGACY-HERMES.md +0 -1099
  67. package/orchestrator/code-graph-watcher.cjs +0 -121
  68. package/orchestrator/tests/code-graph-watcher.test.cjs +0 -183
@@ -23,6 +23,8 @@ families:
23
23
  - { name: task, description: 'authoritative task lifecycle on state.db', phase: '2' }
24
24
  - { name: session, description: 'execution-unit lifecycle with single-active-lease admission (D33)', phase: '4.5' }
25
25
  - { name: plan, description: 'execution-plan artifact export and query (D34)', phase: '8a' }
26
+ - { name: change, description: 'continuous post-delivery change lifecycle and convergence', phase: '9' }
27
+ - { name: system, description: 'read-only diagnostics and explicit backup-first repair', phase: '9' }
26
28
 
27
29
  tools:
28
30
 
@@ -48,6 +50,7 @@ tools:
48
50
  parent_id: { type: string, description: 'Phase 8A.3 parent reference for task.expand children' }
49
51
  tag: { type: string, description: 'git branch tag (Phase 7.2)' }
50
52
  trace_to: { type: string, description: 'spec anchor (e.g. spec/mcp-tools.yaml#task.create)' }
53
+ change_id: { type: string, pattern: '^[a-zA-Z0-9_\-]+$', description: 'continuous change unit owning this task' }
51
54
  output_schema:
52
55
  type: object
53
56
  required: [id, status, created_at]
@@ -90,6 +93,7 @@ tools:
90
93
  stale: { type: boolean }
91
94
  tag: { type: string }
92
95
  completion_commit: { type: string, pattern: '^[0-9a-f]{7,40}$' }
96
+ change_id: { type: ['string', 'null'], pattern: '^[a-zA-Z0-9_\-]+$' }
93
97
  output_schema:
94
98
  type: object
95
99
  required: [ok, task]
@@ -245,12 +249,15 @@ tools:
245
249
  description: override path to the template root (defaults to bundled templates/.ultra/)
246
250
  output_schema:
247
251
  type: object
248
- required: [created_path, status, copied_files]
252
+ required: [created_path, state_db_path, status, copied_files]
249
253
  additionalProperties: false
250
254
  properties:
251
255
  created_path:
252
256
  type: string
253
257
  description: absolute path to the created `.ultra/` directory
258
+ state_db_path:
259
+ type: string
260
+ description: absolute path to the initialized authoritative SQLite database
254
261
  status:
255
262
  type: string
256
263
  enum: [created, overwritten]
@@ -358,6 +365,7 @@ tools:
358
365
  additionalProperties: false
359
366
  properties:
360
367
  task_id: { type: string }
368
+ change_id: { type: string }
361
369
  session_id: { type: string }
362
370
  runtime: { type: string, enum: [claude, opencode, codex] }
363
371
  type:
@@ -385,6 +393,11 @@ tools:
385
393
  - cost_alert
386
394
  - token_alert
387
395
  - circuit_open
396
+ - change_created
397
+ - change_updated
398
+ - change_blocked
399
+ - change_converged
400
+ - change_archived
388
401
  payload: { type: object }
389
402
  output_schema:
390
403
  type: object
@@ -614,6 +627,329 @@ tools:
614
627
  emits_events: []
615
628
  trace_to: '§7.3, D31'
616
629
 
630
+ # ────────────────────────── change family ──────────────────────────────
631
+ - name: change.create
632
+ family: change
633
+ description: Create a continuous feature, fix, redesign, or incident change unit and its initial context manifest.
634
+ phase: '9'
635
+ writer_role: mcp
636
+ input_schema:
637
+ type: object
638
+ required: [id, title, kind, intent]
639
+ additionalProperties: false
640
+ properties:
641
+ id: { type: string, pattern: '^[a-zA-Z0-9_\-]+$' }
642
+ title: { type: string, minLength: 3 }
643
+ kind: { type: string, enum: [quick, standard, major, incident] }
644
+ intent: { type: string, minLength: 3 }
645
+ base_commit: { type: string, pattern: '^[0-9a-f]{7,40}$' }
646
+ docs_impact:
647
+ type: object
648
+ additionalProperties: false
649
+ properties:
650
+ status: { type: string, enum: [unknown, required, none] }
651
+ files: { type: array, items: { type: string } }
652
+ rationale: { type: ['string', 'null'] }
653
+ provider_refs:
654
+ type: object
655
+ additionalProperties: false
656
+ properties:
657
+ memory:
658
+ type: object
659
+ required: [provider]
660
+ additionalProperties: false
661
+ properties:
662
+ provider: { type: string }
663
+ project: { type: string }
664
+ reference: { type: string }
665
+ revision: { type: string }
666
+ indexed_head: { type: string }
667
+ status: { type: string, enum: [available, unavailable, fresh, stale, unknown] }
668
+ code_graph:
669
+ type: object
670
+ required: [provider]
671
+ additionalProperties: false
672
+ properties:
673
+ provider: { type: string }
674
+ project: { type: string }
675
+ reference: { type: string }
676
+ revision: { type: string }
677
+ indexed_head: { type: string }
678
+ status: { type: string, enum: [available, unavailable, fresh, stale, unknown] }
679
+ output_schema:
680
+ type: object
681
+ required: [change, intent_path, context_manifest_path]
682
+ additionalProperties: false
683
+ properties:
684
+ change: { type: object }
685
+ intent_path: { type: string }
686
+ context_manifest_path: { type: string }
687
+ errors:
688
+ - { code: DUPLICATE_CHANGE_ID, description: 'change id already exists' }
689
+ - { code: PROVIDER_CONTENT_FORBIDDEN, description: 'provider payload content must remain external' }
690
+ - { code: VALIDATION_ERROR, description: 'change input failed validation' }
691
+ cli_subcommand: change create
692
+ emits_events: [change_created, context_updated]
693
+
694
+ - name: change.update
695
+ family: change
696
+ description: Update change intent, documentation impact, external provider metadata, or active status.
697
+ phase: '9'
698
+ writer_role: mcp
699
+ input_schema:
700
+ type: object
701
+ required: [id, patch]
702
+ additionalProperties: false
703
+ properties:
704
+ id: { type: string }
705
+ patch:
706
+ type: object
707
+ additionalProperties: false
708
+ properties:
709
+ title: { type: string, minLength: 3 }
710
+ intent: { type: string, minLength: 3 }
711
+ status: { type: string, enum: [active, blocked, cancelled] }
712
+ docs_impact:
713
+ type: object
714
+ additionalProperties: false
715
+ properties:
716
+ status: { type: string, enum: [unknown, required, none] }
717
+ files: { type: array, items: { type: string } }
718
+ rationale: { type: ['string', 'null'] }
719
+ provider_refs:
720
+ type: object
721
+ additionalProperties: false
722
+ properties:
723
+ memory:
724
+ type: object
725
+ required: [provider]
726
+ additionalProperties: false
727
+ properties:
728
+ provider: { type: string }
729
+ project: { type: string }
730
+ reference: { type: string }
731
+ revision: { type: string }
732
+ indexed_head: { type: string }
733
+ status: { type: string, enum: [available, unavailable, fresh, stale, unknown] }
734
+ code_graph:
735
+ type: object
736
+ required: [provider]
737
+ additionalProperties: false
738
+ properties:
739
+ provider: { type: string }
740
+ project: { type: string }
741
+ reference: { type: string }
742
+ revision: { type: string }
743
+ indexed_head: { type: string }
744
+ status: { type: string, enum: [available, unavailable, fresh, stale, unknown] }
745
+ output_schema:
746
+ type: object
747
+ required: [ok, change]
748
+ additionalProperties: false
749
+ properties:
750
+ ok: { type: boolean }
751
+ change: { type: object }
752
+ errors:
753
+ - { code: CHANGE_NOT_FOUND, description: 'change id does not exist' }
754
+ - { code: CHANGE_NOT_MUTABLE, description: 'ready, archived, and cancelled changes are immutable' }
755
+ - { code: ILLEGAL_CHANGE_TRANSITION, description: 'change transition is not permitted' }
756
+ - { code: PROVIDER_CONTENT_FORBIDDEN, description: 'provider payload content must remain external' }
757
+ cli_subcommand: change update
758
+ emits_events: [change_updated]
759
+
760
+ - name: change.get
761
+ family: change
762
+ description: Read one continuous change unit from authoritative state.
763
+ phase: '9'
764
+ readonly: true
765
+ input_schema:
766
+ type: object
767
+ required: [id]
768
+ additionalProperties: false
769
+ properties:
770
+ id: { type: string }
771
+ output_schema:
772
+ type: object
773
+ required: [change]
774
+ additionalProperties: false
775
+ properties:
776
+ change: { type: object }
777
+ errors:
778
+ - { code: CHANGE_NOT_FOUND, description: 'change id does not exist' }
779
+ cli_subcommand: change get
780
+ emits_events: []
781
+
782
+ - name: change.list
783
+ family: change
784
+ description: List continuous changes by lifecycle status or change kind.
785
+ phase: '9'
786
+ readonly: true
787
+ input_schema:
788
+ type: object
789
+ additionalProperties: false
790
+ properties:
791
+ status: { type: string, enum: [active, blocked, ready, archived, cancelled] }
792
+ kind: { type: string, enum: [quick, standard, major, incident] }
793
+ limit: { type: integer, minimum: 1, maximum: 500 }
794
+ output_schema:
795
+ type: object
796
+ required: [changes, count]
797
+ additionalProperties: false
798
+ properties:
799
+ changes: { type: array, items: { type: object } }
800
+ count: { type: integer, minimum: 0 }
801
+ errors:
802
+ - { code: VALIDATION_ERROR, description: 'change filter failed validation' }
803
+ cli_subcommand: change list
804
+ emits_events: []
805
+
806
+ - name: change.context
807
+ family: change
808
+ description: Compile an inspectable task context manifest from Ultra state and external provider metadata references.
809
+ phase: '9'
810
+ writer_role: mcp
811
+ input_schema:
812
+ type: object
813
+ required: [id]
814
+ additionalProperties: false
815
+ properties:
816
+ id: { type: string }
817
+ task_id: { type: string }
818
+ spec_refs: { type: array, items: { type: object } }
819
+ allowed_paths: { type: array, items: { type: string } }
820
+ provider_refs:
821
+ type: object
822
+ additionalProperties: false
823
+ properties:
824
+ memory:
825
+ type: object
826
+ required: [provider]
827
+ additionalProperties: false
828
+ properties:
829
+ provider: { type: string }
830
+ project: { type: string }
831
+ reference: { type: string }
832
+ revision: { type: string }
833
+ indexed_head: { type: string }
834
+ status: { type: string, enum: [available, unavailable, fresh, stale, unknown] }
835
+ code_graph:
836
+ type: object
837
+ required: [provider]
838
+ additionalProperties: false
839
+ properties:
840
+ provider: { type: string }
841
+ project: { type: string }
842
+ reference: { type: string }
843
+ revision: { type: string }
844
+ indexed_head: { type: string }
845
+ status: { type: string, enum: [available, unavailable, fresh, stale, unknown] }
846
+ output_schema:
847
+ type: object
848
+ required: [manifest_path, manifest_hash, manifest]
849
+ additionalProperties: false
850
+ properties:
851
+ manifest_path: { type: string }
852
+ manifest_hash: { type: string, pattern: '^[0-9a-f]{64}$' }
853
+ manifest: { type: object }
854
+ errors:
855
+ - { code: CHANGE_NOT_FOUND, description: 'change id does not exist' }
856
+ - { code: CHANGE_TERMINAL, description: 'terminal change cannot compile a new context' }
857
+ - { code: PROVIDER_CONTENT_FORBIDDEN, description: 'provider payload content must remain external' }
858
+ cli_subcommand: change context
859
+ emits_events: [context_updated]
860
+
861
+ - name: change.converge
862
+ family: change
863
+ description: Deterministically verify task, spec, test, documentation, review, and context evidence before closure.
864
+ phase: '9'
865
+ writer_role: mcp
866
+ input_schema:
867
+ type: object
868
+ required: [id, evidence]
869
+ additionalProperties: false
870
+ properties:
871
+ id: { type: string }
872
+ evidence:
873
+ type: array
874
+ minItems: 1
875
+ items:
876
+ type: object
877
+ required: [category, status, evidence]
878
+ additionalProperties: false
879
+ properties:
880
+ category: { type: string, enum: [diagnosis, diff, tests, spec, docs, review] }
881
+ status: { type: string, enum: [pass, fail, not_applicable] }
882
+ evidence: { type: string, minLength: 3 }
883
+ output_schema:
884
+ type: object
885
+ required: [ready, status, blockers]
886
+ additionalProperties: false
887
+ properties:
888
+ ready: { type: boolean }
889
+ status: { type: string, enum: [blocked, ready] }
890
+ blockers: { type: array, items: { type: string } }
891
+ verification_path: { type: string }
892
+ errors:
893
+ - { code: CHANGE_NOT_FOUND, description: 'change id does not exist' }
894
+ - { code: CHANGE_NOT_CONVERGEABLE, description: 'change lifecycle does not allow convergence' }
895
+ cli_subcommand: change converge
896
+ emits_events: [change_blocked, change_converged]
897
+
898
+ - name: change.archive
899
+ family: change
900
+ description: Archive a converged change after explicit baseline-spec reconciliation.
901
+ phase: '9'
902
+ writer_role: mcp
903
+ input_schema:
904
+ type: object
905
+ required: [id, summary]
906
+ additionalProperties: false
907
+ properties:
908
+ id: { type: string }
909
+ summary: { type: string, minLength: 3 }
910
+ baseline_updates: { type: array, items: { type: string } }
911
+ no_baseline_change_reason: { type: string, minLength: 3 }
912
+ output_schema:
913
+ type: object
914
+ required: [change, archive_path]
915
+ additionalProperties: false
916
+ properties:
917
+ change: { type: object }
918
+ archive_path: { type: string }
919
+ errors:
920
+ - { code: CHANGE_NOT_READY, description: 'change has not passed convergence' }
921
+ - { code: BASELINE_RECONCILIATION_REQUIRED, description: 'baseline update or explicit no-change reason required' }
922
+ - { code: ARCHIVE_EXISTS, description: 'archive target already exists' }
923
+ cli_subcommand: change archive
924
+ emits_events: [change_archived]
925
+
926
+ # ────────────────────────── system family ──────────────────────────────
927
+ - name: system.doctor
928
+ family: system
929
+ description: Inspect Ultra runtime health and optionally perform backup-first recovery and projection repair.
930
+ phase: '9'
931
+ writer_role: mcp
932
+ input_schema:
933
+ type: object
934
+ additionalProperties: false
935
+ properties:
936
+ repair: { type: boolean, default: false }
937
+ output_schema:
938
+ type: object
939
+ required: [status, checks, repair_performed]
940
+ additionalProperties: true
941
+ properties:
942
+ status: { type: string, enum: [healthy, degraded] }
943
+ checks: { type: object }
944
+ repair_performed: { type: boolean }
945
+ backup_path: { type: string }
946
+ repair: { type: object }
947
+ errors:
948
+ - { code: BACKUP_FAILED, description: 'state database backup failed before repair' }
949
+ - { code: STATE_DB_ERROR, description: 'runtime state could not be inspected' }
950
+ cli_subcommand: system doctor
951
+ emits_events: []
952
+
617
953
  # ────────────────────────── plan family (D34) ───────────────────────────
618
954
  - name: plan.export
619
955
  family: plan
@@ -60,7 +60,7 @@
60
60
  },
61
61
  "family": {
62
62
  "type": "string",
63
- "enum": ["task", "memory", "review", "impact", "skill", "session", "ask", "plan"]
63
+ "enum": ["task", "memory", "review", "impact", "skill", "session", "ask", "plan", "change", "system"]
64
64
  },
65
65
  "description": { "type": "string", "minLength": 10 },
66
66
  "phase": {
@@ -12,6 +12,28 @@
12
12
  -- PRAGMA busy_timeout=5000; -- block up to 5s on lock (R25)
13
13
  -- PRAGMA foreign_keys=ON; -- enforce FK constraints
14
14
 
15
+ -- ──────────────────────────── changes ─────────────────────────────────────
16
+ -- A project baseline is continuous; each feature, fix, incident, or redesign
17
+ -- is an independently converged change unit. Only changes become terminal.
18
+ CREATE TABLE IF NOT EXISTS changes (
19
+ id TEXT PRIMARY KEY,
20
+ title TEXT NOT NULL,
21
+ kind TEXT NOT NULL CHECK (kind IN ('quick', 'standard', 'major', 'incident')),
22
+ status TEXT NOT NULL DEFAULT 'active'
23
+ CHECK (status IN ('active', 'blocked', 'ready', 'archived', 'cancelled')),
24
+ intent TEXT NOT NULL,
25
+ docs_impact_json TEXT NOT NULL DEFAULT '{"status":"unknown","files":[],"rationale":null}',
26
+ provider_refs_json TEXT NOT NULL DEFAULT '{}',
27
+ base_commit TEXT,
28
+ artifact_root TEXT NOT NULL,
29
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
30
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
31
+ closed_at TEXT
32
+ );
33
+
34
+ CREATE INDEX IF NOT EXISTS changes_status ON changes(status, created_at);
35
+ CREATE INDEX IF NOT EXISTS changes_kind ON changes(kind, created_at);
36
+
15
37
  -- ──────────────────────────── tasks ───────────────────────────────────────
16
38
  -- Authoritative task row. tasks.json is generated from this table by the
17
39
  -- projector (Phase 2.6). Manual edits to tasks.json are overwritten.
@@ -33,6 +55,7 @@ CREATE TABLE IF NOT EXISTS tasks (
33
55
  trace_to TEXT, -- spec anchor reference
34
56
  context_file TEXT, -- projection target path
35
57
  completion_commit TEXT, -- backfilled hash (Phase 2.8)
58
+ change_id TEXT REFERENCES changes(id) ON DELETE SET NULL,
36
59
  parent_id TEXT REFERENCES tasks(id) ON DELETE SET NULL, -- Phase 8A.1: task.expand subtask→parent
37
60
  created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
38
61
  updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
@@ -43,6 +66,7 @@ CREATE INDEX IF NOT EXISTS tasks_tag ON tasks(tag);
43
66
  CREATE INDEX IF NOT EXISTS tasks_session ON tasks(session_id) WHERE session_id IS NOT NULL;
44
67
  CREATE INDEX IF NOT EXISTS tasks_stale ON tasks(stale) WHERE stale = 1;
45
68
  CREATE INDEX IF NOT EXISTS tasks_parent ON tasks(parent_id) WHERE parent_id IS NOT NULL;
69
+ CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL;
46
70
 
47
71
  -- ──────────────────────────── events ──────────────────────────────────────
48
72
  -- Append-only event stream. id is the subscription cursor (D31, R26):
@@ -53,6 +77,7 @@ CREATE TABLE IF NOT EXISTS events (
53
77
  ts TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
54
78
  type TEXT NOT NULL,
55
79
  task_id TEXT,
80
+ change_id TEXT,
56
81
  session_id TEXT,
57
82
  runtime TEXT CHECK (runtime IS NULL OR runtime IN ('claude', 'opencode', 'codex')),
58
83
  payload_json TEXT
@@ -61,6 +86,7 @@ CREATE TABLE IF NOT EXISTS events (
61
86
  CREATE INDEX IF NOT EXISTS events_ts_type ON events(ts, type);
62
87
  CREATE INDEX IF NOT EXISTS events_task ON events(task_id, id);
63
88
  CREATE INDEX IF NOT EXISTS events_session ON events(session_id, id);
89
+ CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id);
64
90
 
65
91
  -- ──────────────────────────── sessions ────────────────────────────────────
66
92
  -- Authoritative session row. lease_expires_at + heartbeat_at live ONLY here
@@ -146,6 +172,95 @@ CREATE TABLE IF NOT EXISTS circuit_breaker (
146
172
  CREATE INDEX IF NOT EXISTS circuit_breaker_tripped ON circuit_breaker(tripped_at)
147
173
  WHERE tripped_at IS NOT NULL;
148
174
 
175
+ -- ──────────────────────────── change artifacts ────────────────────────────
176
+ CREATE TABLE IF NOT EXISTS artifacts (
177
+ id TEXT PRIMARY KEY,
178
+ change_id TEXT NOT NULL REFERENCES changes(id) ON DELETE CASCADE,
179
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
180
+ kind TEXT NOT NULL,
181
+ path TEXT NOT NULL,
182
+ content_hash TEXT,
183
+ metadata_json TEXT,
184
+ status TEXT NOT NULL DEFAULT 'current' CHECK (status IN ('current', 'archived')),
185
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
186
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
187
+ UNIQUE(change_id, kind, path)
188
+ );
189
+
190
+ CREATE INDEX IF NOT EXISTS artifacts_change ON artifacts(change_id, kind);
191
+
192
+ CREATE TABLE IF NOT EXISTS context_snapshots (
193
+ id TEXT PRIMARY KEY,
194
+ change_id TEXT NOT NULL REFERENCES changes(id) ON DELETE CASCADE,
195
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
196
+ git_head TEXT,
197
+ provider_refs_json TEXT NOT NULL DEFAULT '{}',
198
+ manifest_path TEXT NOT NULL,
199
+ manifest_hash TEXT NOT NULL,
200
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
201
+ );
202
+
203
+ CREATE INDEX IF NOT EXISTS context_snapshots_change ON context_snapshots(change_id, created_at);
204
+
205
+ CREATE TABLE IF NOT EXISTS trace_links (
206
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
207
+ change_id TEXT NOT NULL REFERENCES changes(id) ON DELETE CASCADE,
208
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
209
+ source_ref TEXT NOT NULL,
210
+ target_ref TEXT NOT NULL,
211
+ relation TEXT NOT NULL,
212
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
213
+ UNIQUE(change_id, source_ref, target_ref, relation)
214
+ );
215
+
216
+ CREATE INDEX IF NOT EXISTS trace_links_change ON trace_links(change_id, task_id);
217
+
218
+ -- ──────────────────────────── runtime reliability ─────────────────────────
219
+ CREATE TABLE IF NOT EXISTS incidents (
220
+ id TEXT PRIMARY KEY,
221
+ code TEXT NOT NULL,
222
+ severity TEXT NOT NULL CHECK (severity IN ('warning', 'error', 'critical')),
223
+ status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'resolved')),
224
+ retryable INTEGER NOT NULL DEFAULT 0 CHECK (retryable IN (0, 1)),
225
+ message TEXT NOT NULL,
226
+ change_id TEXT REFERENCES changes(id) ON DELETE SET NULL,
227
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
228
+ session_id TEXT REFERENCES sessions(sid) ON DELETE SET NULL,
229
+ source_kind TEXT,
230
+ source_id TEXT,
231
+ evidence_json TEXT,
232
+ occurrence_count INTEGER NOT NULL DEFAULT 1,
233
+ first_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
234
+ last_seen_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
235
+ resolved_at TEXT,
236
+ resolution TEXT
237
+ );
238
+
239
+ CREATE INDEX IF NOT EXISTS incidents_open ON incidents(status, severity, last_seen_at);
240
+ CREATE INDEX IF NOT EXISTS incidents_change ON incidents(change_id, status);
241
+
242
+ CREATE TABLE IF NOT EXISTS projection_jobs (
243
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
244
+ tool_name TEXT NOT NULL,
245
+ event_cursor INTEGER NOT NULL DEFAULT 0,
246
+ status TEXT NOT NULL DEFAULT 'pending'
247
+ CHECK (status IN ('pending', 'running', 'completed', 'failed')),
248
+ attempts INTEGER NOT NULL DEFAULT 0,
249
+ max_attempts INTEGER NOT NULL DEFAULT 3 CHECK (max_attempts > 0),
250
+ last_error TEXT,
251
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
252
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
253
+ completed_at TEXT
254
+ );
255
+
256
+ CREATE INDEX IF NOT EXISTS projection_jobs_status ON projection_jobs(status, id);
257
+
258
+ CREATE TABLE IF NOT EXISTS event_consumers (
259
+ name TEXT PRIMARY KEY,
260
+ cursor INTEGER NOT NULL DEFAULT 0 CHECK (cursor >= 0),
261
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
262
+ );
263
+
149
264
  -- ──────────────────────────── seed: schema_version ────────────────────────
150
265
  INSERT OR IGNORE INTO schema_version (version, description)
151
266
  VALUES ('4.5', 'Phase 2 initial — tasks/events/sessions/schema_version/migration_history/telemetry/specs_refs');
@@ -155,3 +270,5 @@ INSERT OR IGNORE INTO schema_version (version, description)
155
270
  VALUES ('8A.1', 'Phase 8A.1 — tasks.parent_id for task.expand parent→children');
156
271
  INSERT OR IGNORE INTO schema_version (version, description)
157
272
  VALUES ('8A.2', 'Phase 8A.2 — tasks.estimated_days preserved across MCP and v4.4 migration');
273
+ INSERT OR IGNORE INTO schema_version (version, description)
274
+ VALUES ('9.0', 'Continuous change units, context snapshots, trace links, incidents, projection outbox, and durable consumers');
@@ -35,6 +35,7 @@ USAGE:
35
35
  SUBCOMMANDS:
36
36
  task init-project
37
37
  session close | get | list | admission | heartbeat | subscribe | reap
38
+ system doctor [--repair]
38
39
  status [--cost] [--since <duration>] [--json]
39
40
  db init | checkpoint | vacuum | integrity | backup (Phase 2)
40
41
  migrate --from=4.4 --to=4.5 [--dry|--rollback] (Phase 2)
@@ -65,6 +66,7 @@ const taskCommand = require('./commands/task.cjs');
65
66
  const sessionCommand = require('./commands/session.cjs');
66
67
  const statusCommand = require('./commands/status.cjs');
67
68
  const legacyMemoryCommand = require('./commands/legacy-memory.cjs');
69
+ const systemCommand = require('./commands/system.cjs');
68
70
 
69
71
  // Phase 6.2 — CLI telemetry: best-effort, never blocks the subcommand.
70
72
  function emitCliTelemetry(sub, rest) {
@@ -93,6 +95,7 @@ const SUBCOMMANDS = {
93
95
  db: (args) => process.exit(dbCommand.dispatch(args)),
94
96
  migrate: (args) => process.exit(migrateCommand.dispatch(args)),
95
97
  'legacy-memory': (args) => process.exit(legacyMemoryCommand.dispatch(args)),
98
+ system: (args) => systemCommand.dispatch(args).then((code) => process.exit(code)),
96
99
  };
97
100
 
98
101
  function main(argv) {
@@ -0,0 +1,57 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const { openStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
7
+ const doctor = require('../../mcp-server/lib/doctor.cjs');
8
+
9
+ const USAGE = `ultra-tools system doctor [--repair]\n`;
10
+
11
+ function emit(envelope) {
12
+ process.stdout.write(`${JSON.stringify(envelope)}\n`);
13
+ }
14
+
15
+ async function dispatchDoctor(args) {
16
+ const unknown = args.filter((arg) => !['--repair', '-h', '--help'].includes(arg));
17
+ if (args.includes('-h') || args.includes('--help')) {
18
+ process.stdout.write(USAGE);
19
+ return 0;
20
+ }
21
+ if (unknown.length > 0) {
22
+ emit({ ok: false, error: { code: 'VALIDATION_ERROR', message: `unknown system doctor flag: ${unknown[0]}` } });
23
+ return 1;
24
+ }
25
+ const rootDir = process.cwd();
26
+ const dbPath = path.join(rootDir, '.ultra', 'state.db');
27
+ if (!fs.existsSync(dbPath)) {
28
+ emit({ ok: false, error: { code: 'STATE_DB_MISSING', message: `.ultra/state.db not found under ${rootDir}` } });
29
+ return 2;
30
+ }
31
+ const db = openStateDb(dbPath);
32
+ try {
33
+ const data = await doctor.runDoctor(db, { rootDir, repair: args.includes('--repair') });
34
+ emit({ ok: true, data });
35
+ return data.status === 'healthy' ? 0 : 2;
36
+ } catch (error) {
37
+ emit({ ok: false, error: { code: error.code || 'STATE_DB_ERROR', message: error.message } });
38
+ return 2;
39
+ } finally {
40
+ closeStateDb(db);
41
+ }
42
+ }
43
+
44
+ async function dispatch(args) {
45
+ const [verb, ...rest] = args;
46
+ if (!verb || verb === '-h' || verb === '--help') {
47
+ process.stdout.write(USAGE);
48
+ return 0;
49
+ }
50
+ if (verb !== 'doctor') {
51
+ emit({ ok: false, error: { code: 'UNKNOWN_VERB', message: `unknown system verb: ${verb}` } });
52
+ return 1;
53
+ }
54
+ return dispatchDoctor(rest);
55
+ }
56
+
57
+ module.exports = { USAGE, dispatch, dispatchDoctor };