ultra-builder-pro-cli 0.19.1 → 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 (103) 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 +90 -1
  5. package/README.md +62 -29
  6. package/adapters/_shared/codex-assets.cjs +69 -11
  7. package/adapters/_shared/interaction-contract.cjs +60 -5
  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/commands/ultra-research.md +1 -1
  15. package/docs/AGENT-CONTEXT.md +79 -45
  16. package/docs/ARCHITECTURE.md +75 -38
  17. package/docs/ARTIFACT-AUTHORITY.md +234 -0
  18. package/docs/DECISIONS.md +20 -10
  19. package/docs/LEGACY-CLI-CROSSWALK.md +15 -11
  20. package/docs/PLUGIN-ISOLATION-CONTRACT.md +4 -3
  21. package/docs/ROADMAP.md +21 -6
  22. package/docs/RUNTIME-COMPAT-MATRIX.md +30 -13
  23. package/docs/STATE-DB-ACCESS-POLICY.md +11 -9
  24. package/docs/WORKFLOW-LIFECYCLE.md +105 -54
  25. package/hooks/README.md +6 -5
  26. package/hooks/active_task_context.py +97 -12
  27. package/hooks/context_spine.py +16 -4
  28. package/hooks/health_check.py +5 -4
  29. package/hooks/pre_stop_check.py +5 -2
  30. package/hooks/runtime_paths.py +396 -0
  31. package/hooks/subagent_tracker.py +5 -2
  32. package/hooks/workflow_checkpoint.py +16 -3
  33. package/hooks/workflow_context.py +11 -3
  34. package/hooks/workflow_resume.py +7 -4
  35. package/mcp-server/breadcrumb.cjs +20 -2
  36. package/mcp-server/lib/archive-journal.cjs +1026 -111
  37. package/mcp-server/lib/archive-mutation-worker.py +365 -0
  38. package/mcp-server/lib/artifact-registry.cjs +1126 -0
  39. package/mcp-server/lib/baseline-workflow.cjs +9 -4
  40. package/mcp-server/lib/change-packet.cjs +818 -0
  41. package/mcp-server/lib/change-workflow.cjs +932 -176
  42. package/mcp-server/lib/context-paths.cjs +111 -0
  43. package/mcp-server/lib/context-spine.cjs +511 -45
  44. package/mcp-server/lib/decision-dialogue.cjs +427 -12
  45. package/mcp-server/lib/delivery-transaction.cjs +471 -0
  46. package/mcp-server/lib/doctor-backup-worker.cjs +328 -0
  47. package/mcp-server/lib/doctor.cjs +375 -12
  48. package/mcp-server/lib/git-bootstrap.cjs +459 -47
  49. package/mcp-server/lib/init-project.cjs +190 -95
  50. package/mcp-server/lib/plan-publication-worker.cjs +830 -0
  51. package/mcp-server/lib/plan-store.cjs +700 -5
  52. package/mcp-server/lib/project-breadcrumb.cjs +23 -4
  53. package/mcp-server/lib/projector.cjs +174 -40
  54. package/mcp-server/lib/runtime-paths.cjs +1687 -0
  55. package/mcp-server/lib/safe-project-file.cjs +385 -0
  56. package/mcp-server/lib/spec-learning.cjs +28 -12
  57. package/mcp-server/lib/state-db.cjs +397 -13
  58. package/mcp-server/lib/state-ops.cjs +248 -49
  59. package/mcp-server/lib/telemetry.cjs +12 -3
  60. package/mcp-server/lib/workflow-state.cjs +614 -62
  61. package/mcp-server/server.cjs +282 -33
  62. package/orchestrator/daemon.cjs +2 -10
  63. package/orchestrator/parallel-orchestrator.cjs +314 -50
  64. package/orchestrator/planner/plan-builder.cjs +1 -1
  65. package/orchestrator/recovery.cjs +125 -7
  66. package/orchestrator/session-close-journal-worker.cjs +5 -0
  67. package/orchestrator/session-close-journal.cjs +1014 -0
  68. package/orchestrator/session-runner.cjs +679 -69
  69. package/orchestrator/worktree-manager.cjs +186 -35
  70. package/package.json +2 -3
  71. package/skills/cc-collab/SKILL.md +4 -1
  72. package/skills/codex-collab/SKILL.md +5 -2
  73. package/skills/ultra-change/SKILL.md +38 -13
  74. package/skills/ultra-deliver/SKILL.md +23 -11
  75. package/skills/ultra-dev/SKILL.md +33 -6
  76. package/skills/ultra-doctor/SKILL.md +6 -2
  77. package/skills/ultra-init/SKILL.md +4 -2
  78. package/skills/ultra-plan/SKILL.md +68 -18
  79. package/skills/ultra-research/SKILL.md +45 -32
  80. package/skills/ultra-review/SKILL.md +9 -5
  81. package/skills/ultra-status/SKILL.md +9 -4
  82. package/skills/ultra-test/SKILL.md +15 -7
  83. package/skills/ultra-think/SKILL.md +6 -0
  84. package/skills/ultra-think/references/decision-dialogue.md +61 -14
  85. package/skills/ultra-verify/SKILL.md +4 -1
  86. package/spec/cli-protocol.md +19 -7
  87. package/spec/mcp-tools.yaml +531 -46
  88. package/spec/schemas/change-delta.v1.schema.json +153 -0
  89. package/spec/schemas/documentation-reconciliation.v1.schema.json +106 -0
  90. package/spec/schemas/mcp-tools.schema.json +1 -1
  91. package/spec/schemas/state-db.sql +57 -17
  92. package/spec/schemas/tasks.v4.5.schema.json +1 -1
  93. package/ultra-tools/cli.cjs +3 -2
  94. package/ultra-tools/commands/db.cjs +76 -25
  95. package/ultra-tools/commands/migrate.cjs +995 -80
  96. package/ultra-tools/commands/session.cjs +34 -8
  97. package/ultra-tools/commands/status.cjs +34 -7
  98. package/ultra-tools/commands/system.cjs +129 -22
  99. package/commands/learn.md +0 -10
  100. package/orchestrator/skill-miner.cjs +0 -133
  101. package/output-styles/coding-vibes.md +0 -67
  102. package/output-styles/structural-thinking.md +0 -61
  103. 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 = '18.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';
@@ -19,6 +19,9 @@ const AUTHORITY_SCHEMA_VERSION = '14.0';
19
19
  const SEMANTIC_SCHEMA_VERSION = '15.0';
20
20
  const DIALOGUE_SCHEMA_VERSION = '16.0';
21
21
  const GIT_AUTHORITY_SCHEMA_VERSION = '17.0';
22
+ const ADAPTIVE_SCHEMA_VERSION = '18.0';
23
+ const DECISION_COMPLETION_SCHEMA_VERSION = '19.0';
24
+ const ARTIFACT_REGISTRY_SCHEMA_VERSION = '20.0';
22
25
 
23
26
  const MIGRATED_GAPS = Object.freeze([{
24
27
  id: 'legacy-rebaseline-required',
@@ -42,6 +45,7 @@ const REQUIRED_TABLES = Object.freeze([
42
45
  'circuit_breaker',
43
46
  'changes',
44
47
  'artifacts',
48
+ 'artifact_edges',
45
49
  'context_snapshots',
46
50
  'spec_learning_candidates',
47
51
  'trace_links',
@@ -54,6 +58,93 @@ const REQUIRED_TABLES = Object.freeze([
54
58
  'decision_items',
55
59
  ]);
56
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
+
57
148
  function applyBaselineUpgrade(db, { legacyState = false } = {}) {
58
149
  db.exec(`
59
150
  CREATE TABLE IF NOT EXISTS baselines (
@@ -235,6 +326,7 @@ function tableNames(db) {
235
326
  }
236
327
 
237
328
  function applySchema(db) {
329
+ assertNoDuplicateActiveSessionLeases(db);
238
330
  runScript(db, readSchemaSql());
239
331
  }
240
332
 
@@ -328,7 +420,12 @@ function migrateAdaptiveWorkflowRuns(db, fromVersion = latestSchemaVersion(db))
328
420
  const existing = db.prepare(
329
421
  "SELECT 1 FROM migration_history WHERE to_version = '18.0' AND status = 'success' LIMIT 1",
330
422
  ).get();
331
- if (fromVersion === EXPECTED_VERSION || existing) return false;
423
+ if ([
424
+ ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
425
+ ARTIFACT_REGISTRY_SCHEMA_VERSION,
426
+ ].includes(fromVersion) || existing) {
427
+ return false;
428
+ }
332
429
 
333
430
  const obsoleteSteps = Object.freeze({
334
431
  init: ['establish-baseline'],
@@ -455,6 +552,95 @@ function baselineSupportsUnbornGit(db) {
455
552
  return typeof row?.sql === 'string' && row.sql.includes("'unborn'");
456
553
  }
457
554
 
555
+ function decisionThreadsSupportCompletion(db) {
556
+ const row = db.prepare(
557
+ "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'decision_threads'",
558
+ ).get();
559
+ if (typeof row?.sql !== 'string' || !row.sql.includes("'completed'")) return false;
560
+ return columnNames(db, 'decision_threads').has('completed_at');
561
+ }
562
+
563
+ function upgradeDecisionThreadLifecycle(db) {
564
+ if (!tableNames(db).includes('decision_threads') || decisionThreadsSupportCompletion(db)) {
565
+ return false;
566
+ }
567
+ const foreignKeys = db.pragma('foreign_keys', { simple: true });
568
+ db.pragma('foreign_keys = OFF');
569
+ try {
570
+ db.transaction(() => {
571
+ db.exec(`
572
+ DROP TABLE IF EXISTS decision_threads_completion_upgrade;
573
+ CREATE TABLE decision_threads_completion_upgrade (
574
+ id TEXT PRIMARY KEY,
575
+ purpose TEXT NOT NULL,
576
+ mode TEXT NOT NULL DEFAULT 'guided'
577
+ CHECK (mode IN ('guided', 'fast', 'autonomous', 'diagnostic')),
578
+ status TEXT NOT NULL DEFAULT 'active'
579
+ CHECK (status IN ('active', 'checkpoint_ready', 'completed', 'confirmed', 'cancelled')),
580
+ baseline_id TEXT REFERENCES baselines(id) ON DELETE SET NULL,
581
+ change_id TEXT REFERENCES changes(id) ON DELETE CASCADE,
582
+ workflow_run_id TEXT REFERENCES workflow_runs(id) ON DELETE CASCADE,
583
+ summary_json TEXT NOT NULL DEFAULT '{}',
584
+ checkpoint_json TEXT NOT NULL DEFAULT '{}',
585
+ started_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
586
+ updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')),
587
+ completed_at TEXT,
588
+ confirmed_at TEXT,
589
+ CHECK (baseline_id IS NOT NULL OR change_id IS NOT NULL OR workflow_run_id IS NOT NULL)
590
+ );
591
+ INSERT INTO decision_threads_completion_upgrade (
592
+ id, purpose, mode, status, baseline_id, change_id, workflow_run_id,
593
+ summary_json, checkpoint_json, started_at, updated_at, completed_at, confirmed_at
594
+ )
595
+ SELECT
596
+ id, purpose, mode, status, baseline_id, change_id, workflow_run_id,
597
+ summary_json, checkpoint_json, started_at, updated_at, NULL, confirmed_at
598
+ FROM decision_threads;
599
+ DROP TABLE decision_threads;
600
+ ALTER TABLE decision_threads_completion_upgrade RENAME TO decision_threads;
601
+ CREATE INDEX decision_threads_status
602
+ ON decision_threads(status, updated_at);
603
+ CREATE INDEX decision_threads_authority
604
+ ON decision_threads(baseline_id, change_id, workflow_run_id, status);
605
+ `);
606
+ db.prepare(
607
+ `UPDATE decision_threads
608
+ SET status = 'completed',
609
+ summary_json = CASE
610
+ WHEN summary_json = '{}' THEN ?
611
+ ELSE json_set(summary_json, '$.completion_kind', 'migrated_settled_thread')
612
+ END,
613
+ completed_at = updated_at
614
+ WHERE status = 'active'
615
+ AND EXISTS (
616
+ SELECT 1 FROM decision_items WHERE decision_items.thread_id = decision_threads.id
617
+ )
618
+ AND NOT EXISTS (
619
+ SELECT 1 FROM decision_items
620
+ WHERE decision_items.thread_id = decision_threads.id AND status = 'open'
621
+ )
622
+ AND NOT EXISTS (
623
+ SELECT 1 FROM decision_items
624
+ WHERE decision_items.thread_id = decision_threads.id
625
+ AND blocking = 1 AND status = 'deferred'
626
+ )`,
627
+ ).run(JSON.stringify({
628
+ text: 'Existing normalized decision state was completed during schema migration.',
629
+ completion_kind: 'migrated_settled_thread',
630
+ }));
631
+ const violations = db.pragma('foreign_key_check');
632
+ if (violations.length > 0) {
633
+ throw new Error(
634
+ `decision completion migration produced ${violations.length} foreign key violation(s)`,
635
+ );
636
+ }
637
+ })();
638
+ } finally {
639
+ db.pragma(`foreign_keys = ${foreignKeys ? 'ON' : 'OFF'}`);
640
+ }
641
+ return true;
642
+ }
643
+
458
644
  function upgradeBaselineWorktreeConstraint(db, fromVersion = latestSchemaVersion(db)) {
459
645
  if (!tableNames(db).includes('baselines') || baselineSupportsUnbornGit(db)) return false;
460
646
  const previousVersion = fromVersion || 'unknown';
@@ -530,6 +716,7 @@ function upgradeBaselineWorktreeConstraint(db, fromVersion = latestSchemaVersion
530
716
  }
531
717
 
532
718
  function upgradeRuntimeConstraints(db, fromVersion = latestSchemaVersion(db)) {
719
+ assertNoDuplicateActiveSessionLeases(db);
533
720
  if (tableSupportsKimi(db, 'events') && tableSupportsKimi(db, 'sessions')) return false;
534
721
  const previousVersion = fromVersion || 'unknown';
535
722
  const foreignKeys = db.pragma('foreign_keys', { simple: true });
@@ -581,6 +768,8 @@ function upgradeRuntimeConstraints(db, fromVersion = latestSchemaVersion(db)) {
581
768
  ALTER TABLE sessions_runtime_upgrade RENAME TO sessions;
582
769
  CREATE INDEX sessions_active ON sessions(status, task_id);
583
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';
584
773
  `);
585
774
  db.prepare(
586
775
  `INSERT INTO migration_history
@@ -602,7 +791,129 @@ function upgradeRuntimeConstraints(db, fromVersion = latestSchemaVersion(db)) {
602
791
  return true;
603
792
  }
604
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
+
605
915
  function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { legacyState = false } = {}) {
916
+ assertNoDuplicateActiveSessionLeases(db);
606
917
  const runtimeChanged = upgradeRuntimeConstraints(db, fromVersion);
607
918
  if (tableNames(db).includes('baselines') && !baselineSupportsUnbornGit(db)) {
608
919
  db.transaction(() => {
@@ -611,6 +922,7 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
611
922
  })();
612
923
  }
613
924
  const gitStateChangedFrom = upgradeBaselineWorktreeConstraint(db, fromVersion);
925
+ const decisionCompletionChanged = upgradeDecisionThreadLifecycle(db);
614
926
  db.transaction(() => {
615
927
  applyCompatibleColumns(db);
616
928
  const contextChanged = applyContextSpineUpgrade(db);
@@ -621,7 +933,8 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
621
933
  fromVersion && ![
622
934
  CONTEXT_SCHEMA_VERSION, BASELINE_SCHEMA_VERSION, ADOPTION_SCHEMA_VERSION,
623
935
  WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION,
624
- DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, EXPECTED_VERSION,
936
+ DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
937
+ DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
625
938
  ].includes(fromVersion)
626
939
  && !contextMigration,
627
940
  );
@@ -640,13 +953,15 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
640
953
  applyBaselineColumns(db);
641
954
  applyChangeColumns(db);
642
955
  applySemanticAuthorityColumns(db);
956
+ const artifactRegistryChanged = applyArtifactRegistryUpgrade(db);
643
957
  const baselineMigration = db.prepare(
644
958
  "SELECT 1 FROM migration_history WHERE to_version = '11.0' AND status = 'success' LIMIT 1",
645
959
  ).get();
646
960
  if (fromVersion && ![
647
961
  BASELINE_SCHEMA_VERSION, ADOPTION_SCHEMA_VERSION, WORKFLOW_SCHEMA_VERSION,
648
962
  AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION,
649
- GIT_AUTHORITY_SCHEMA_VERSION, EXPECTED_VERSION,
963
+ GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
964
+ DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
650
965
  ].includes(fromVersion)
651
966
  && !baselineMigration) {
652
967
  db.prepare(
@@ -715,7 +1030,11 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
715
1030
  const workflowMigration = db.prepare(
716
1031
  "SELECT 1 FROM migration_history WHERE to_version = '13.0' AND status = 'success' LIMIT 1",
717
1032
  ).get();
718
- if (fromVersion && fromVersion !== EXPECTED_VERSION && !workflowMigration) {
1033
+ if (fromVersion && ![
1034
+ WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION,
1035
+ DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
1036
+ DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
1037
+ ].includes(fromVersion) && !workflowMigration) {
719
1038
  db.prepare(
720
1039
  `INSERT INTO migration_history
721
1040
  (from_version, to_version, direction, status, notes)
@@ -729,7 +1048,11 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
729
1048
  const authorityMigration = db.prepare(
730
1049
  "SELECT 1 FROM migration_history WHERE to_version = '14.0' AND status = 'success' LIMIT 1",
731
1050
  ).get();
732
- if (fromVersion && fromVersion !== EXPECTED_VERSION && !authorityMigration) {
1051
+ if (fromVersion && ![
1052
+ AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION,
1053
+ GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
1054
+ ARTIFACT_REGISTRY_SCHEMA_VERSION,
1055
+ ].includes(fromVersion) && !authorityMigration) {
733
1056
  db.prepare(
734
1057
  `INSERT INTO migration_history
735
1058
  (from_version, to_version, direction, status, notes)
@@ -743,7 +1066,11 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
743
1066
  const semanticMigration = db.prepare(
744
1067
  "SELECT 1 FROM migration_history WHERE to_version = '15.0' AND status = 'success' LIMIT 1",
745
1068
  ).get();
746
- if (fromVersion && fromVersion !== EXPECTED_VERSION && !semanticMigration) {
1069
+ if (fromVersion && ![
1070
+ SEMANTIC_SCHEMA_VERSION, DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION,
1071
+ ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
1072
+ ARTIFACT_REGISTRY_SCHEMA_VERSION,
1073
+ ].includes(fromVersion) && !semanticMigration) {
747
1074
  db.prepare(
748
1075
  `INSERT INTO migration_history
749
1076
  (from_version, to_version, direction, status, notes)
@@ -757,7 +1084,10 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
757
1084
  const dialogueMigration = db.prepare(
758
1085
  "SELECT 1 FROM migration_history WHERE to_version = '16.0' AND status = 'success' LIMIT 1",
759
1086
  ).get();
760
- if (fromVersion && ![DIALOGUE_SCHEMA_VERSION, EXPECTED_VERSION].includes(fromVersion)
1087
+ if (fromVersion && ![
1088
+ DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
1089
+ DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
1090
+ ].includes(fromVersion)
761
1091
  && !dialogueMigration) {
762
1092
  db.prepare(
763
1093
  `INSERT INTO migration_history
@@ -772,7 +1102,10 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
772
1102
  const gitStateMigration = db.prepare(
773
1103
  "SELECT 1 FROM migration_history WHERE to_version = '17.0' AND status = 'success' LIMIT 1",
774
1104
  ).get();
775
- if (fromVersion && ![GIT_AUTHORITY_SCHEMA_VERSION, EXPECTED_VERSION].includes(fromVersion)
1105
+ if (fromVersion && ![
1106
+ GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
1107
+ ARTIFACT_REGISTRY_SCHEMA_VERSION,
1108
+ ].includes(fromVersion)
776
1109
  && !gitStateMigration) {
777
1110
  db.prepare(
778
1111
  `INSERT INTO migration_history
@@ -788,26 +1121,65 @@ function applyCompatibleUpgrades(db, fromVersion = latestSchemaVersion(db), { le
788
1121
  const adaptiveMigration = db.prepare(
789
1122
  "SELECT 1 FROM migration_history WHERE to_version = '18.0' AND status = 'success' LIMIT 1",
790
1123
  ).get();
791
- if (fromVersion && fromVersion !== EXPECTED_VERSION && !adaptiveMigration) {
1124
+ if (fromVersion && ![
1125
+ ADAPTIVE_SCHEMA_VERSION, DECISION_COMPLETION_SCHEMA_VERSION,
1126
+ ARTIFACT_REGISTRY_SCHEMA_VERSION,
1127
+ ].includes(fromVersion) && !adaptiveMigration) {
792
1128
  db.prepare(
793
1129
  `INSERT INTO migration_history
794
1130
  (from_version, to_version, direction, status, notes)
795
1131
  VALUES (?, ?, 'forward', 'success', ?)`,
796
1132
  ).run(
797
1133
  fromVersion === GIT_AUTHORITY_SCHEMA_VERSION ? fromVersion : GIT_AUTHORITY_SCHEMA_VERSION,
798
- EXPECTED_VERSION,
1134
+ ADAPTIVE_SCHEMA_VERSION,
799
1135
  adaptiveChanged
800
1136
  ? 'Migrate active rigid workflows to adaptive capability transitions and independent initialization'
801
1137
  : 'Add adaptive capability transitions and independent initialization authority',
802
1138
  );
803
1139
  }
1140
+ const completionMigration = db.prepare(
1141
+ "SELECT 1 FROM migration_history WHERE to_version = '19.0' AND status = 'success' LIMIT 1",
1142
+ ).get();
1143
+ if (fromVersion && ![
1144
+ DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
1145
+ ].includes(fromVersion) && !completionMigration) {
1146
+ db.prepare(
1147
+ `INSERT INTO migration_history
1148
+ (from_version, to_version, direction, status, notes)
1149
+ VALUES (?, ?, 'forward', 'success', ?)`,
1150
+ ).run(
1151
+ ADAPTIVE_SCHEMA_VERSION,
1152
+ DECISION_COMPLETION_SCHEMA_VERSION,
1153
+ decisionCompletionChanged
1154
+ ? 'Add non-ceremonial decision completion and migrate settled active threads'
1155
+ : 'Add non-ceremonial decision completion authority',
1156
+ );
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
+ }
804
1175
  db.exec('CREATE INDEX IF NOT EXISTS tasks_change ON tasks(change_id) WHERE change_id IS NOT NULL');
805
1176
  db.exec('CREATE INDEX IF NOT EXISTS events_change ON events(change_id, id)');
1177
+ ensureActiveSessionLeaseIndex(db);
806
1178
  db.prepare(
807
1179
  'INSERT OR IGNORE INTO schema_version (version, description) VALUES (?, ?)',
808
1180
  ).run(
809
1181
  EXPECTED_VERSION,
810
- 'Adaptive capability transitions, independent initialization, and recoverable workflow migration',
1182
+ 'Typed artifact registry, normalized dependency edges, and orphan diagnostics',
811
1183
  );
812
1184
  })();
813
1185
  }
@@ -834,13 +1206,22 @@ function initStateDb(dbPath) {
834
1206
  db = openStateDb(dbPath);
835
1207
  const existing = new Set(tableNames(db));
836
1208
  const fromVersion = latestSchemaVersion(db);
1209
+ assertNoDuplicateActiveArtifactPaths(db);
837
1210
  const legacyState = existing.size > 0
838
1211
  && ![
839
1212
  WORKFLOW_SCHEMA_VERSION, AUTHORITY_SCHEMA_VERSION, SEMANTIC_SCHEMA_VERSION,
840
- DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, EXPECTED_VERSION,
1213
+ DIALOGUE_SCHEMA_VERSION, GIT_AUTHORITY_SCHEMA_VERSION, ADAPTIVE_SCHEMA_VERSION,
1214
+ DECISION_COMPLETION_SCHEMA_VERSION, ARTIFACT_REGISTRY_SCHEMA_VERSION,
841
1215
  ].includes(fromVersion);
842
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);
843
1221
  applyCompatibleColumns(db);
1222
+ if (existing.has('artifacts')) {
1223
+ db.transaction(() => applyArtifactRegistryUpgrade(db))();
1224
+ }
844
1225
  const missing = REQUIRED_TABLES.filter((t) => !existing.has(t));
845
1226
  if (missing.length > 0) {
846
1227
  applySchema(db);
@@ -884,4 +1265,7 @@ module.exports = {
884
1265
  tableNames,
885
1266
  runScript,
886
1267
  MIGRATED_GAPS,
1268
+ ActiveSessionLeaseConflictError,
1269
+ activeSessionLeaseConflicts,
1270
+ assertNoDuplicateActiveSessionLeases,
887
1271
  };