ultra-builder-pro-cli 0.20.0 → 0.21.1

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 (102) hide show
  1. package/.ultra-template/docs/research/README.md +26 -5
  2. package/.ultra-template/specs/discovery.md +1 -1
  3. package/.ultra-template/tasks/tasks.json +1 -1
  4. package/CHANGELOG.md +66 -0
  5. package/README.md +54 -24
  6. package/adapters/_shared/codex-assets.cjs +69 -11
  7. package/adapters/_shared/interaction-contract.cjs +36 -3
  8. package/adapters/_shared/runtime-assets.cjs +41 -1
  9. package/adapters/claude.js +7 -5
  10. package/adapters/codex.js +1 -1
  11. package/adapters/kimi.js +2 -8
  12. package/adapters/opencode.js +30 -28
  13. package/bin/orchestrator.js +64 -24
  14. package/docs/AGENT-CONTEXT.md +74 -43
  15. package/docs/ARCHITECTURE.md +63 -31
  16. package/docs/ARTIFACT-AUTHORITY.md +234 -0
  17. package/docs/DECISIONS.md +10 -4
  18. package/docs/LEGACY-CLI-CROSSWALK.md +6 -4
  19. package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
  20. package/docs/ROADMAP.md +19 -5
  21. package/docs/RUNTIME-COMPAT-MATRIX.md +24 -13
  22. package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
  23. package/docs/WORKFLOW-LIFECYCLE.md +70 -39
  24. package/hooks/README.md +6 -5
  25. package/hooks/active_task_context.py +97 -12
  26. package/hooks/context_spine.py +16 -4
  27. package/hooks/health_check.py +5 -4
  28. package/hooks/pre_stop_check.py +5 -2
  29. package/hooks/runtime_paths.py +396 -0
  30. package/hooks/subagent_tracker.py +5 -2
  31. package/hooks/workflow_checkpoint.py +16 -3
  32. package/hooks/workflow_context.py +11 -3
  33. package/hooks/workflow_resume.py +7 -4
  34. package/mcp-server/breadcrumb.cjs +20 -2
  35. package/mcp-server/lib/archive-journal.cjs +1026 -111
  36. package/mcp-server/lib/archive-mutation-worker.py +365 -0
  37. package/mcp-server/lib/artifact-registry.cjs +1126 -0
  38. package/mcp-server/lib/baseline-workflow.cjs +9 -4
  39. package/mcp-server/lib/change-packet.cjs +818 -0
  40. package/mcp-server/lib/change-workflow.cjs +932 -176
  41. package/mcp-server/lib/context-paths.cjs +111 -0
  42. package/mcp-server/lib/context-spine.cjs +511 -45
  43. package/mcp-server/lib/decision-dialogue.cjs +345 -3
  44. package/mcp-server/lib/delivery-transaction.cjs +471 -0
  45. package/mcp-server/lib/doctor-backup-worker.cjs +328 -0
  46. package/mcp-server/lib/doctor.cjs +373 -11
  47. package/mcp-server/lib/git-bootstrap.cjs +459 -47
  48. package/mcp-server/lib/init-project.cjs +190 -95
  49. package/mcp-server/lib/plan-publication-worker.cjs +830 -0
  50. package/mcp-server/lib/plan-store.cjs +700 -5
  51. package/mcp-server/lib/project-breadcrumb.cjs +23 -4
  52. package/mcp-server/lib/projector.cjs +174 -40
  53. package/mcp-server/lib/runtime-paths.cjs +1687 -0
  54. package/mcp-server/lib/safe-project-file.cjs +385 -0
  55. package/mcp-server/lib/spec-learning.cjs +28 -12
  56. package/mcp-server/lib/state-db.cjs +263 -9
  57. package/mcp-server/lib/state-ops.cjs +248 -49
  58. package/mcp-server/lib/telemetry.cjs +12 -3
  59. package/mcp-server/lib/workflow-state.cjs +610 -58
  60. package/mcp-server/server.cjs +278 -33
  61. package/orchestrator/daemon.cjs +2 -10
  62. package/orchestrator/parallel-orchestrator.cjs +314 -50
  63. package/orchestrator/planner/plan-builder.cjs +1 -1
  64. package/orchestrator/recovery.cjs +125 -7
  65. package/orchestrator/session-close-journal-worker.cjs +5 -0
  66. package/orchestrator/session-close-journal.cjs +1014 -0
  67. package/orchestrator/session-runner.cjs +679 -69
  68. package/orchestrator/worktree-manager.cjs +186 -35
  69. package/package.json +2 -3
  70. package/skills/cc-collab/SKILL.md +4 -1
  71. package/skills/codex-collab/SKILL.md +5 -2
  72. package/skills/ultra-change/SKILL.md +26 -8
  73. package/skills/ultra-deliver/SKILL.md +23 -11
  74. package/skills/ultra-dev/SKILL.md +33 -6
  75. package/skills/ultra-doctor/SKILL.md +6 -2
  76. package/skills/ultra-init/SKILL.md +4 -2
  77. package/skills/ultra-plan/SKILL.md +49 -16
  78. package/skills/ultra-research/SKILL.md +30 -27
  79. package/skills/ultra-review/SKILL.md +9 -5
  80. package/skills/ultra-status/SKILL.md +9 -4
  81. package/skills/ultra-test/SKILL.md +10 -5
  82. package/skills/ultra-think/SKILL.md +6 -2
  83. package/skills/ultra-think/references/decision-dialogue.md +17 -6
  84. package/skills/ultra-verify/SKILL.md +4 -1
  85. package/spec/cli-protocol.md +18 -7
  86. package/spec/mcp-tools.yaml +502 -44
  87. package/spec/schemas/change-delta.v1.schema.json +153 -0
  88. package/spec/schemas/documentation-reconciliation.v1.schema.json +106 -0
  89. package/spec/schemas/mcp-tools.schema.json +1 -1
  90. package/spec/schemas/state-db.sql +50 -13
  91. package/spec/schemas/tasks.v4.5.schema.json +1 -1
  92. package/ultra-tools/cli.cjs +3 -2
  93. package/ultra-tools/commands/db.cjs +76 -25
  94. package/ultra-tools/commands/migrate.cjs +995 -80
  95. package/ultra-tools/commands/session.cjs +34 -8
  96. package/ultra-tools/commands/status.cjs +32 -6
  97. package/ultra-tools/commands/system.cjs +129 -22
  98. package/commands/learn.md +0 -10
  99. package/orchestrator/skill-miner.cjs +0 -133
  100. package/output-styles/coding-vibes.md +0 -67
  101. package/output-styles/structural-thinking.md +0 -61
  102. package/skills/learn/SKILL.md +0 -58
@@ -9,7 +9,7 @@ const REPO_ROOT = process.env.UBP_RUNTIME_ROOT
9
9
  ? path.resolve(process.env.UBP_RUNTIME_ROOT)
10
10
  : path.resolve(__dirname, '..', '..');
11
11
  const SCHEMA_FILE = path.join(REPO_ROOT, 'spec', 'schemas', 'state-db.sql');
12
- const EXPECTED_VERSION = '19.0';
12
+ const EXPECTED_VERSION = '20.0';
13
13
  const KIMI_SCHEMA_VERSION = '9.1';
14
14
  const CONTEXT_SCHEMA_VERSION = '10.0';
15
15
  const BASELINE_SCHEMA_VERSION = '11.0';
@@ -21,6 +21,7 @@ const DIALOGUE_SCHEMA_VERSION = '16.0';
21
21
  const GIT_AUTHORITY_SCHEMA_VERSION = '17.0';
22
22
  const ADAPTIVE_SCHEMA_VERSION = '18.0';
23
23
  const DECISION_COMPLETION_SCHEMA_VERSION = '19.0';
24
+ const ARTIFACT_REGISTRY_SCHEMA_VERSION = '20.0';
24
25
 
25
26
  const MIGRATED_GAPS = Object.freeze([{
26
27
  id: 'legacy-rebaseline-required',
@@ -44,6 +45,7 @@ const REQUIRED_TABLES = Object.freeze([
44
45
  'circuit_breaker',
45
46
  'changes',
46
47
  'artifacts',
48
+ 'artifact_edges',
47
49
  'context_snapshots',
48
50
  'spec_learning_candidates',
49
51
  'trace_links',
@@ -56,6 +58,93 @@ const REQUIRED_TABLES = Object.freeze([
56
58
  'decision_items',
57
59
  ]);
58
60
 
61
+ class ActiveSessionLeaseConflictError extends Error {
62
+ constructor(conflicts) {
63
+ super(
64
+ 'state.db has duplicate active task leases; resolve the conflicting sessions before upgrade: '
65
+ + conflicts.map((row) => `${row.task_id}=[${row.session_ids.join(',')}]`).join('; '),
66
+ );
67
+ this.name = 'ActiveSessionLeaseConflictError';
68
+ this.code = 'ACTIVE_SESSION_LEASE_CONFLICT';
69
+ this.details = { conflicts };
70
+ }
71
+ }
72
+
73
+ class ArtifactAuthorityConflictError extends Error {
74
+ constructor(conflicts) {
75
+ super(
76
+ 'state.db has duplicate active artifact authority for canonical paths: '
77
+ + conflicts.map((row) => `${row.path}=[${row.artifact_ids.join(',')}]`).join('; '),
78
+ );
79
+ this.name = 'ArtifactAuthorityConflictError';
80
+ this.code = 'ARTIFACT_AUTHORITY_CONFLICT';
81
+ this.details = { conflicts };
82
+ }
83
+ }
84
+
85
+ function canonicalArtifactPath(value) {
86
+ const raw = String(value || '').trim().replaceAll('\\', '/');
87
+ if (!raw || path.posix.isAbsolute(raw)) {
88
+ throw new ArtifactAuthorityConflictError([{
89
+ path: raw || '(empty)',
90
+ artifact_ids: [],
91
+ reason: 'invalid_path',
92
+ }]);
93
+ }
94
+ const normalized = path.posix.normalize(raw).replace(/^\.\//, '');
95
+ if (normalized === '..' || normalized.startsWith('../') || normalized.includes('/../')) {
96
+ throw new ArtifactAuthorityConflictError([{
97
+ path: raw,
98
+ artifact_ids: [],
99
+ reason: 'path_escape',
100
+ }]);
101
+ }
102
+ return normalized;
103
+ }
104
+
105
+ function activeArtifactPathConflicts(db) {
106
+ if (!tableNames(db).includes('artifacts')) return [];
107
+ const columns = columnNames(db, 'artifacts');
108
+ if (!columns.has('id') || !columns.has('path')) return [];
109
+ const hasStatus = columns.has('status');
110
+ const rows = db.prepare(
111
+ `SELECT id, path FROM artifacts${hasStatus ? " WHERE status <> 'archived'" : ''}
112
+ ORDER BY id`,
113
+ ).all();
114
+ const grouped = new Map();
115
+ for (const row of rows) {
116
+ const canonical = canonicalArtifactPath(row.path);
117
+ const ids = grouped.get(canonical) || [];
118
+ ids.push(row.id);
119
+ grouped.set(canonical, ids);
120
+ }
121
+ return [...grouped.entries()]
122
+ .filter(([, ids]) => ids.length > 1)
123
+ .map(([artifactPath, ids]) => ({ path: artifactPath, artifact_ids: ids }));
124
+ }
125
+
126
+ function assertNoDuplicateActiveArtifactPaths(db) {
127
+ const conflicts = activeArtifactPathConflicts(db);
128
+ if (conflicts.length > 0) throw new ArtifactAuthorityConflictError(conflicts);
129
+ return true;
130
+ }
131
+
132
+ function normalizeStoredArtifactPaths(db) {
133
+ if (!tableNames(db).includes('artifacts')) return false;
134
+ assertNoDuplicateActiveArtifactPaths(db);
135
+ const rows = db.prepare('SELECT id, path FROM artifacts ORDER BY id').all();
136
+ let changed = false;
137
+ const update = db.prepare('UPDATE artifacts SET path = ? WHERE id = ?');
138
+ for (const row of rows) {
139
+ const canonical = canonicalArtifactPath(row.path);
140
+ if (canonical !== row.path) {
141
+ update.run(canonical, row.id);
142
+ changed = true;
143
+ }
144
+ }
145
+ return changed;
146
+ }
147
+
59
148
  function applyBaselineUpgrade(db, { legacyState = false } = {}) {
60
149
  db.exec(`
61
150
  CREATE TABLE IF NOT EXISTS baselines (
@@ -237,6 +326,7 @@ function tableNames(db) {
237
326
  }
238
327
 
239
328
  function applySchema(db) {
329
+ assertNoDuplicateActiveSessionLeases(db);
240
330
  runScript(db, readSchemaSql());
241
331
  }
242
332
 
@@ -330,7 +420,10 @@ function migrateAdaptiveWorkflowRuns(db, fromVersion = latestSchemaVersion(db))
330
420
  const existing = db.prepare(
331
421
  "SELECT 1 FROM migration_history WHERE to_version = '18.0' AND status = 'success' LIMIT 1",
332
422
  ).get();
333
- if ([ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION].includes(fromVersion) || existing) {
423
+ if ([
424
+ ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
425
+ ARTIFACT_REGISTRY_SCHEMA_VERSION,
426
+ ].includes(fromVersion) || existing) {
334
427
  return false;
335
428
  }
336
429
 
@@ -623,6 +716,7 @@ function upgradeBaselineWorktreeConstraint(db, fromVersion = latestSchemaVersion
623
716
  }
624
717
 
625
718
  function upgradeRuntimeConstraints(db, fromVersion = latestSchemaVersion(db)) {
719
+ assertNoDuplicateActiveSessionLeases(db);
626
720
  if (tableSupportsKimi(db, 'events') && tableSupportsKimi(db, 'sessions')) return false;
627
721
  const previousVersion = fromVersion || 'unknown';
628
722
  const foreignKeys = db.pragma('foreign_keys', { simple: true });
@@ -674,6 +768,8 @@ function upgradeRuntimeConstraints(db, fromVersion = latestSchemaVersion(db)) {
674
768
  ALTER TABLE sessions_runtime_upgrade RENAME TO sessions;
675
769
  CREATE INDEX sessions_active ON sessions(status, task_id);
676
770
  CREATE INDEX sessions_lease ON sessions(lease_expires_at) WHERE status = 'running';
771
+ CREATE UNIQUE INDEX sessions_one_active_task
772
+ ON sessions(task_id) WHERE status = 'running';
677
773
  `);
678
774
  db.prepare(
679
775
  `INSERT INTO migration_history
@@ -695,7 +791,129 @@ function upgradeRuntimeConstraints(db, fromVersion = latestSchemaVersion(db)) {
695
791
  return true;
696
792
  }
697
793
 
794
+ function activeSessionLeaseConflicts(db) {
795
+ if (!tableNames(db).includes('sessions')) return [];
796
+ const columns = columnNames(db, 'sessions');
797
+ if (!['sid', 'task_id', 'status'].every((name) => columns.has(name))) return [];
798
+ const rows = db.prepare(
799
+ `SELECT task_id, sid
800
+ FROM sessions
801
+ WHERE status = 'running'
802
+ ORDER BY task_id, sid`,
803
+ ).all();
804
+ const byTask = new Map();
805
+ for (const row of rows) {
806
+ const ids = byTask.get(row.task_id) || [];
807
+ ids.push(row.sid);
808
+ byTask.set(row.task_id, ids);
809
+ }
810
+ return [...byTask.entries()]
811
+ .filter(([, sessionIds]) => sessionIds.length > 1)
812
+ .map(([taskId, sessionIds]) => ({
813
+ task_id: taskId,
814
+ lease_count: sessionIds.length,
815
+ session_ids: sessionIds,
816
+ }));
817
+ }
818
+
819
+ function assertNoDuplicateActiveSessionLeases(db) {
820
+ const conflicts = activeSessionLeaseConflicts(db);
821
+ if (conflicts.length > 0) throw new ActiveSessionLeaseConflictError(conflicts);
822
+ return true;
823
+ }
824
+
825
+ function ensureActiveSessionLeaseIndex(db) {
826
+ if (!tableNames(db).includes('sessions')) return false;
827
+ assertNoDuplicateActiveSessionLeases(db);
828
+ const existing = db.prepare(
829
+ "SELECT 1 AS present FROM sqlite_master WHERE type = 'index' AND name = 'sessions_one_active_task'",
830
+ ).get();
831
+ if (existing) return false;
832
+ db.exec(
833
+ `CREATE UNIQUE INDEX sessions_one_active_task
834
+ ON sessions(task_id) WHERE status = 'running'`,
835
+ );
836
+ return true;
837
+ }
838
+
839
+ function applyArtifactRegistryUpgrade(db) {
840
+ if (!tableNames(db).includes('artifacts')) return false;
841
+ assertNoDuplicateActiveArtifactPaths(db);
842
+ const columns = columnNames(db, 'artifacts');
843
+ let changed = false;
844
+ if (![
845
+ 'owner_type', 'owner_id', 'digest', 'before_digest', 'after_digest',
846
+ 'provenance_json', 'managed',
847
+ ].every((column) => columns.has(column))) {
848
+ db.exec(`
849
+ DROP TABLE IF EXISTS artifacts_registry_upgrade;
850
+ CREATE TABLE artifacts_registry_upgrade (
851
+ id TEXT PRIMARY KEY,
852
+ owner_type TEXT NOT NULL
853
+ CHECK (owner_type IN ('project', 'baseline', 'change', 'task', 'workflow')),
854
+ owner_id TEXT NOT NULL,
855
+ change_id TEXT REFERENCES changes(id) ON DELETE SET NULL,
856
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
857
+ kind TEXT NOT NULL,
858
+ path TEXT NOT NULL,
859
+ digest TEXT,
860
+ content_hash TEXT,
861
+ before_digest TEXT,
862
+ after_digest TEXT,
863
+ provenance_json TEXT NOT NULL DEFAULT '{}',
864
+ metadata_json TEXT,
865
+ managed INTEGER NOT NULL DEFAULT 0 CHECK (managed IN (0, 1)),
866
+ status TEXT NOT NULL DEFAULT 'current'
867
+ CHECK (status IN ('current', 'stale', 'terminal', 'archived')),
868
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
869
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
870
+ UNIQUE(owner_type, owner_id, kind, path)
871
+ );
872
+ INSERT INTO artifacts_registry_upgrade
873
+ (id, owner_type, owner_id, change_id, task_id, kind, path, digest,
874
+ content_hash, before_digest, after_digest, provenance_json,
875
+ metadata_json, managed, status, created_at, updated_at)
876
+ SELECT id,
877
+ CASE WHEN task_id IS NOT NULL THEN 'task' ELSE 'change' END,
878
+ CASE WHEN task_id IS NOT NULL THEN task_id ELSE change_id END,
879
+ change_id, task_id, kind, path, content_hash, content_hash,
880
+ NULL, content_hash, '{"migration":"schema-20.0"}',
881
+ metadata_json, 0, status, created_at, updated_at
882
+ FROM artifacts;
883
+ DROP TABLE artifacts;
884
+ ALTER TABLE artifacts_registry_upgrade RENAME TO artifacts;
885
+ `);
886
+ changed = true;
887
+ }
888
+ if (normalizeStoredArtifactPaths(db)) changed = true;
889
+ db.exec(`
890
+ CREATE INDEX IF NOT EXISTS artifacts_change ON artifacts(change_id, kind);
891
+ CREATE INDEX IF NOT EXISTS artifacts_owner ON artifacts(owner_type, owner_id, status);
892
+ CREATE INDEX IF NOT EXISTS artifacts_path ON artifacts(path, status);
893
+ CREATE UNIQUE INDEX IF NOT EXISTS artifacts_one_active_path
894
+ ON artifacts(path) WHERE status <> 'archived';
895
+ CREATE TABLE IF NOT EXISTS artifact_edges (
896
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
897
+ source_type TEXT NOT NULL
898
+ CHECK (source_type IN ('artifact', 'project', 'baseline', 'change', 'task', 'workflow', 'external')),
899
+ source_id TEXT NOT NULL,
900
+ target_type TEXT NOT NULL
901
+ CHECK (target_type IN ('artifact', 'project', 'baseline', 'change', 'task', 'workflow', 'external')),
902
+ target_id TEXT NOT NULL,
903
+ relation TEXT NOT NULL,
904
+ created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
905
+ UNIQUE(source_type, source_id, target_type, target_id, relation)
906
+ );
907
+ CREATE INDEX IF NOT EXISTS artifact_edges_source
908
+ ON artifact_edges(source_type, source_id);
909
+ CREATE INDEX IF NOT EXISTS artifact_edges_target
910
+ ON artifact_edges(target_type, target_id);
911
+ `);
912
+ return changed;
913
+ }
914
+
698
915
  function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { legacyState = false } = {}) {
916
+ assertNoDuplicateActiveSessionLeases(db);
699
917
  const runtimeChanged = upgradeRuntimeConstraints(db, fromVersion);
700
918
  if (tableNames(db).includes('baselines') && !baselineSupportsUnbornGit(db)) {
701
919
  db.transaction(() => {
@@ -716,7 +934,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
716
934
  CONTEXT_SCHEMA_VERSION, BASELINE_SCHEMA_VERSION, ADOPTION_SCHEMA_VERSION,
717
935
  WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION,
718
936
  DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
719
- DECISION_COMPLETION_SCHEMA_VERSION,
937
+ DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
720
938
  ].includes(fromVersion)
721
939
  && !contextMigration,
722
940
  );
@@ -735,6 +953,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
735
953
  applyBaselineColumns(db);
736
954
  applyChangeColumns(db);
737
955
  applySemanticAuthorityColumns(db);
956
+ const artifactRegistryChanged = applyArtifactRegistryUpgrade(db);
738
957
  const baselineMigration = db.prepare(
739
958
  "SELECT 1 FROM migration_history WHERE to_version = '11.0' AND status = 'success' LIMIT 1",
740
959
  ).get();
@@ -742,7 +961,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
742
961
  BASELINE_SCHEMA_VERSION, ADOPTION_SCHEMA_VERSION, WORKFLOW_SCHEMA_VERSION,
743
962
  AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION,
744
963
  GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
745
- DECISION_COMPLETION_SCHEMA_VERSION,
964
+ DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
746
965
  ].includes(fromVersion)
747
966
  && !baselineMigration) {
748
967
  db.prepare(
@@ -814,7 +1033,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
814
1033
  if (fromVersion && ![
815
1034
  WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION,
816
1035
  DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
817
- DECISION_COMPLETION_SCHEMA_VERSION,
1036
+ DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
818
1037
  ].includes(fromVersion) && !workflowMigration) {
819
1038
  db.prepare(
820
1039
  `INSERT INTO migration_history
@@ -832,6 +1051,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
832
1051
  if (fromVersion && ![
833
1052
  AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION,
834
1053
  GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
1054
+ ARTIFACT_REGISTRY_SCHEMA_VERSION,
835
1055
  ].includes(fromVersion) && !authorityMigration) {
836
1056
  db.prepare(
837
1057
  `INSERT INTO migration_history
@@ -849,6 +1069,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
849
1069
  if (fromVersion && ![
850
1070
  SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION,
851
1071
  ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
1072
+ ARTIFACT_REGISTRY_SCHEMA_VERSION,
852
1073
  ].includes(fromVersion) && !semanticMigration) {
853
1074
  db.prepare(
854
1075
  `INSERT INTO migration_history
@@ -865,7 +1086,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
865
1086
  ).get();
866
1087
  if (fromVersion && ![
867
1088
  DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
868
- DECISION_COMPLETION_SCHEMA_VERSION,
1089
+ DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
869
1090
  ].includes(fromVersion)
870
1091
  && !dialogueMigration) {
871
1092
  db.prepare(
@@ -883,6 +1104,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
883
1104
  ).get();
884
1105
  if (fromVersion && ![
885
1106
  GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
1107
+ ARTIFACT_REGISTRY_SCHEMA_VERSION,
886
1108
  ].includes(fromVersion)
887
1109
  && !gitStateMigration) {
888
1110
  db.prepare(
@@ -901,6 +1123,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
901
1123
  ).get();
902
1124
  if (fromVersion && ![
903
1125
  ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
1126
+ ARTIFACT_REGISTRY_SCHEMA_VERSION,
904
1127
  ].includes(fromVersion) && !adaptiveMigration) {
905
1128
  db.prepare(
906
1129
  `INSERT INTO migration_history
@@ -917,7 +1140,9 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
917
1140
  const completionMigration = db.prepare(
918
1141
  "SELECT 1 FROM migration_history WHERE to_version = '19.0' AND status = 'success' LIMIT 1",
919
1142
  ).get();
920
- if (fromVersion && fromVersion !== DECISION_COMPLETION_SCHEMA_VERSION && !completionMigration) {
1143
+ if (fromVersion && ![
1144
+ DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
1145
+ ].includes(fromVersion) && !completionMigration) {
921
1146
  db.prepare(
922
1147
  `INSERT INTO migration_history
923
1148
  (from_version, to_version, direction, status, notes)
@@ -930,13 +1155,31 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
930
1155
  : 'Add non-ceremonial decision completion authority',
931
1156
  );
932
1157
  }
1158
+ const artifactRegistryMigration = db.prepare(
1159
+ "SELECT 1 FROM migration_history WHERE to_version = '20.0' AND status = 'success' LIMIT 1",
1160
+ ).get();
1161
+ if (fromVersion && fromVersion !== ARTIFACT_REGISTRY_SCHEMA_VERSION
1162
+ && !artifactRegistryMigration) {
1163
+ db.prepare(
1164
+ `INSERT INTO migration_history
1165
+ (from_version, to_version, direction, status, notes)
1166
+ VALUES (?, ?, 'forward', 'success', ?)`,
1167
+ ).run(
1168
+ DECISION_COMPLETION_SCHEMA_VERSION,
1169
+ ARTIFACT_REGISTRY_SCHEMA_VERSION,
1170
+ artifactRegistryChanged
1171
+ ? 'Migrate legacy change artifacts into the typed artifact registry and dependency graph'
1172
+ : 'Add the typed artifact registry, dependency graph, and orphan diagnostics',
1173
+ );
1174
+ }
933
1175
  db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
934
1176
  db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
1177
+ ensureActiveSessionLeaseIndex(db);
935
1178
  db.prepare(
936
1179
  'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
937
1180
  ).run(
938
1181
  EXPECTED_VERSION,
939
- 'Non-ceremonial decision completion and host-neutral intent persistence',
1182
+ 'Typed artifact registry, normalized dependency edges, and orphan diagnostics',
940
1183
  );
941
1184
  })();
942
1185
  }
@@ -963,14 +1206,22 @@ function initStateDb(dbPath) {
963
1206
  db = openStateDb(dbPath);
964
1207
  const existing = new Set(tableNames(db));
965
1208
  const fromVersion = latestSchemaVersion(db);
1209
+ assertNoDuplicateActiveArtifactPaths(db);
966
1210
  const legacyState = existing.size > 0
967
1211
  && ![
968
1212
  WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION,
969
1213
  DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
970
- DECISION_COMPLETION_SCHEMA_VERSION,
1214
+ DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
971
1215
  ].includes(fromVersion);
972
1216
  backupPath = migrationBackup(db, dbPath, fromVersion, existing);
1217
+ // This must precede every ALTER, table rebuild, or index creation. Runtime
1218
+ // constraint upgrades rebuild sessions and would otherwise surface a
1219
+ // generic SQLITE_CONSTRAINT instead of the complete authority conflict.
1220
+ assertNoDuplicateActiveSessionLeases(db);
973
1221
  applyCompatibleColumns(db);
1222
+ if (existing.has('artifacts')) {
1223
+ db.transaction(() => applyArtifactRegistryUpgrade(db))();
1224
+ }
974
1225
  const missing = REQUIRED_TABLES.filter((t) => !existing.has(t));
975
1226
  if (missing.length > 0) {
976
1227
  applySchema(db);
@@ -1014,4 +1265,7 @@ module.exports = {
1014
1265
  tableNames,
1015
1266
  runScript,
1016
1267
  MIGRATED_GAPS,
1268
+ ActiveSessionLeaseConflictError,
1269
+ activeSessionLeaseConflicts,
1270
+ assertNoDuplicateActiveSessionLeases,
1017
1271
  };