ultra-builder-pro-cli 0.22.0 → 0.24.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 (93) hide show
  1. package/.ultra-template/docs/research/README.md +4 -4
  2. package/CHANGELOG.md +83 -0
  3. package/README.md +68 -35
  4. package/adapters/_shared/codex-assets.cjs +121 -60
  5. package/adapters/_shared/interaction-contract.cjs +20 -7
  6. package/adapters/_shared/provenance.cjs +7 -2
  7. package/adapters/_shared/runtime-assets.cjs +4 -3
  8. package/adapters/_shared/runtime-native.cjs +132 -0
  9. package/adapters/claude.js +61 -31
  10. package/adapters/codex.js +178 -81
  11. package/adapters/grok.js +372 -0
  12. package/adapters/kimi.js +8 -4
  13. package/adapters/opencode.js +211 -33
  14. package/agents/code-reviewer.md +4 -2
  15. package/agents/debugger.md +4 -1
  16. package/agents/review-code.md +7 -3
  17. package/agents/review-comments.md +7 -4
  18. package/agents/review-coordinator.md +11 -7
  19. package/agents/review-design.md +8 -4
  20. package/agents/review-errors.md +8 -4
  21. package/agents/review-spec.md +8 -5
  22. package/agents/review-tests.md +7 -4
  23. package/agents/tdd-runner.md +5 -2
  24. package/bin/install.js +9 -4
  25. package/bin/orchestrator.js +4 -13
  26. package/docs/AGENT-CONTEXT.md +141 -231
  27. package/docs/ARCHITECTURE.md +203 -498
  28. package/docs/ARTIFACT-AUTHORITY.md +38 -35
  29. package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
  30. package/docs/ROADMAP.md +85 -110
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +139 -174
  32. package/docs/SKILL-AUTHORING.md +3 -3
  33. package/docs/STATE-DB-ACCESS-POLICY.md +25 -20
  34. package/docs/WORKFLOW-LIFECYCLE.md +174 -442
  35. package/hooks/README.md +11 -8
  36. package/hooks/active_task_context.py +16 -16
  37. package/hooks/adapters/grok.py +134 -0
  38. package/hooks/{context_spine.py → context_envelope.py} +26 -22
  39. package/hooks/health_check.py +19 -29
  40. package/hooks/pre_stop_check.py +22 -12
  41. package/hooks/subagent_tracker.py +1 -1
  42. package/hooks/workflow_checkpoint.py +14 -13
  43. package/hooks/workflow_context.py +11 -12
  44. package/hooks/workflow_resume.py +14 -13
  45. package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
  46. package/mcp-server/lib/artifact-registry.cjs +25 -1
  47. package/mcp-server/lib/baseline-workflow.cjs +36 -38
  48. package/mcp-server/lib/canonical-json.cjs +19 -0
  49. package/mcp-server/lib/change-workflow.cjs +637 -49
  50. package/mcp-server/lib/context-envelope.cjs +508 -0
  51. package/mcp-server/lib/decision-records.cjs +337 -0
  52. package/mcp-server/lib/doctor.cjs +91 -33
  53. package/mcp-server/lib/init-project.cjs +78 -39
  54. package/mcp-server/lib/managed-file-write.cjs +116 -0
  55. package/mcp-server/lib/plan-checkpoint.cjs +177 -0
  56. package/mcp-server/lib/project-context.cjs +178 -0
  57. package/mcp-server/lib/stage-checkpoints.cjs +309 -0
  58. package/mcp-server/lib/state-db.cjs +155 -24
  59. package/mcp-server/lib/task-ledger.cjs +522 -13
  60. package/mcp-server/lib/ultra-facade.cjs +834 -0
  61. package/mcp-server/lib/worker-packet.cjs +358 -0
  62. package/mcp-server/server.cjs +208 -1059
  63. package/orchestrator/daemon.cjs +27 -9
  64. package/orchestrator/parallel-orchestrator.cjs +111 -76
  65. package/orchestrator/session-runner.cjs +135 -34
  66. package/package.json +10 -3
  67. package/skills/ultra-change/SKILL.md +39 -98
  68. package/skills/ultra-deliver/SKILL.md +42 -65
  69. package/skills/ultra-dev/SKILL.md +52 -99
  70. package/skills/ultra-doctor/SKILL.md +35 -57
  71. package/skills/ultra-init/SKILL.md +48 -88
  72. package/skills/ultra-init/references/brownfield-adoption.md +11 -8
  73. package/skills/ultra-plan/SKILL.md +54 -132
  74. package/skills/ultra-research/SKILL.md +58 -112
  75. package/skills/ultra-review/SKILL.md +28 -46
  76. package/skills/ultra-review/references/unified-schema.md +2 -0
  77. package/skills/ultra-review/scripts/review_wait.py +8 -0
  78. package/skills/ultra-status/SKILL.md +22 -45
  79. package/skills/ultra-test/SKILL.md +27 -61
  80. package/skills/ultra-think/SKILL.md +29 -48
  81. package/skills/ultra-think/references/interaction-boundary.md +56 -0
  82. package/spec/cli-protocol.md +80 -187
  83. package/spec/mcp-tools.yaml +130 -2738
  84. package/spec/schemas/mcp-tools.schema.json +1 -1
  85. package/spec/schemas/state-db.sql +107 -4
  86. package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
  87. package/ultra-tools/commands/status.cjs +369 -243
  88. package/mcp-server/lib/context-spine.cjs +0 -1208
  89. package/mcp-server/lib/decision-dialogue.cjs +0 -1128
  90. package/mcp-server/lib/project-breadcrumb.cjs +0 -173
  91. package/mcp-server/lib/spec-learning.cjs +0 -284
  92. package/mcp-server/lib/workflow-state.cjs +0 -2965
  93. package/skills/ultra-think/references/decision-dialogue.md +0 -153
@@ -8,8 +8,8 @@ const Ajv = require('ajv/dist/2020');
8
8
 
9
9
  const ops = require('./state-ops.cjs');
10
10
  const artifacts = require('./artifact-registry.cjs');
11
- const contextSpine = require('./context-spine.cjs');
12
- const specLearning = require('./spec-learning.cjs');
11
+ const canonical = require('./canonical-json.cjs');
12
+ const { writeManagedFile } = require('./managed-file-write.cjs');
13
13
  const providerRefs = require('./provider-refs.cjs');
14
14
  const baselines = require('./baseline-workflow.cjs');
15
15
  const archiveJournal = require('./archive-journal.cjs');
@@ -20,13 +20,32 @@ const {
20
20
  readStableProjectFile,
21
21
  walkStableProjectTree,
22
22
  } = require('./safe-project-file.cjs');
23
- const workflows = require('./workflow-state.cjs');
24
- const decisions = require('./decision-dialogue.cjs');
23
+ const stageCheckpoints = require('./stage-checkpoints.cjs');
25
24
  const reconciliationSchema = require('../../spec/schemas/baseline-reconciliation.v1.schema.json');
26
25
 
27
26
  const reconciliationAjv = new Ajv({ allErrors: true, strict: false });
28
27
  const validateReconciliationSchema = reconciliationAjv.compile(reconciliationSchema);
29
28
 
29
+ function loadLegacyModule(filename) {
30
+ return module.require(path.join(__dirname, filename));
31
+ }
32
+
33
+ function legacyContextSpine() {
34
+ return loadLegacyModule('context-spine.cjs');
35
+ }
36
+
37
+ function legacySpecLearning() {
38
+ return loadLegacyModule('spec-learning.cjs');
39
+ }
40
+
41
+ function legacyWorkflows() {
42
+ return loadLegacyModule('workflow-state.cjs');
43
+ }
44
+
45
+ function legacyDecisions() {
46
+ return loadLegacyModule('decision-dialogue.cjs');
47
+ }
48
+
30
49
  const CHANGE_ID = /^[a-zA-Z0-9_-]+$/;
31
50
  const CHANGE_PATCH_FIELDS = new Set([
32
51
  'title', 'intent', 'status', 'docs_impact', 'provider_refs',
@@ -39,7 +58,7 @@ const SEMANTIC_AUTHORITY_FIELDS = new Set([
39
58
  const CHANGE_TRANSITIONS = Object.freeze({
40
59
  active: new Set(['active', 'blocked', 'cancelled']),
41
60
  blocked: new Set(['active', 'blocked', 'cancelled']),
42
- ready: new Set(['ready']),
61
+ ready: new Set(['active', 'ready', 'cancelled']),
43
62
  archived: new Set(),
44
63
  cancelled: new Set(),
45
64
  });
@@ -494,7 +513,13 @@ function readReconciliationManifest(db, change, input, rootDir) {
494
513
  );
495
514
  }
496
515
  let resolved;
497
- try { resolved = workflows.resolveProjectSourceRef(rootDir, item.source_ref, 'semantic_change.source_ref'); }
516
+ try {
517
+ resolved = legacyWorkflows().resolveProjectSourceRef(
518
+ rootDir,
519
+ item.source_ref,
520
+ 'semantic_change.source_ref',
521
+ );
522
+ }
498
523
  catch (error) {
499
524
  throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', error.message);
500
525
  }
@@ -653,14 +678,17 @@ function upsertArtifact(
653
678
  db,
654
679
  {
655
680
  change_id, task_id = null, kind, artifactPath, contentHash, metadata,
656
- workflow_id = null, terminal = false, rootDir = process.cwd(),
681
+ workflow_id = null, consumer_ref = null, terminal = false,
682
+ writer = 'change-workflow', rootDir = process.cwd(),
657
683
  },
658
684
  ) {
659
685
  const ownerType = task_id ? 'task' : 'change';
660
686
  const ownerId = task_id || change_id;
661
687
  const consumer = terminal
662
688
  ? null
663
- : task_id
689
+ : consumer_ref
690
+ ? consumer_ref
691
+ : task_id
664
692
  ? { type: 'task', id: task_id, relation: 'consumed_by' }
665
693
  : workflow_id
666
694
  ? { type: 'workflow', id: workflow_id, relation: 'consumed_by' }
@@ -674,7 +702,7 @@ function upsertArtifact(
674
702
  source_refs: [{ type: 'change', id: change_id, relation: 'produced_for' }],
675
703
  consumer_refs: consumer ? [consumer] : [],
676
704
  provenance: {
677
- writer: 'change-workflow',
705
+ writer,
678
706
  workflow_run_id: consumer?.type === 'workflow' ? consumer.id : null,
679
707
  },
680
708
  metadata: { ...(metadata || {}), ...(terminal ? { terminal_role: true } : {}) },
@@ -698,15 +726,15 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
698
726
  'SELECT id FROM tasks WHERE change_id = ? ORDER BY created_at ASC',
699
727
  ).all(change.id).map((task) => ops.readTask(db, task.id));
700
728
  const specs = Array.isArray(input.spec_refs) ? input.spec_refs : [];
701
- const spine = contextSpine.compileRoleContext(db, { input, change, tasks, rootDir });
729
+ const spine = legacyContextSpine().compileRoleContext(
730
+ db,
731
+ { input, change, tasks, rootDir },
732
+ );
702
733
  const recommendation = spine.recommendation;
703
734
  const durableSpine = { ...spine };
704
735
  delete durableSpine.recommendation;
705
- const snapshotId = `ctx-${crypto.randomUUID().slice(0, 12)}`;
706
- const manifest = {
736
+ const semanticManifest = {
707
737
  schema_version: '3.0',
708
- snapshot_id: snapshotId,
709
- generated_at: nowIso(),
710
738
  change: {
711
739
  id: change.id, title: change.title, kind: change.kind, status: change.status,
712
740
  intent: change.intent, docs_impact: change.docs_impact, base_commit: change.base_commit,
@@ -734,6 +762,43 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
734
762
  providers,
735
763
  provider_boundary: 'metadata references only; memory and code graph content remain external',
736
764
  };
765
+ const identityHash = crypto.createHash('sha256')
766
+ .update(JSON.stringify(semanticManifest))
767
+ .digest('hex');
768
+ const snapshotId = `ctx-${identityHash.slice(0, 12)}`;
769
+ const existing = db.prepare(
770
+ `SELECT manifest_path, manifest_hash
771
+ FROM context_snapshots WHERE id = ?`,
772
+ ).get(snapshotId);
773
+ if (existing) {
774
+ const existingPath = path.resolve(rootDir, existing.manifest_path);
775
+ if (!fs.existsSync(existingPath)) {
776
+ throw new ChangeWorkflowError(
777
+ 'CONTEXT_SNAPSHOT_CORRUPT',
778
+ `content-addressed Context snapshot is missing: ${existing.manifest_path}`,
779
+ );
780
+ }
781
+ const bytes = fs.readFileSync(existingPath);
782
+ const existingHash = crypto.createHash('sha256').update(bytes).digest('hex');
783
+ if (existingHash !== existing.manifest_hash) {
784
+ throw new ChangeWorkflowError(
785
+ 'CONTEXT_SNAPSHOT_CORRUPT',
786
+ `content-addressed Context snapshot digest changed: ${existing.manifest_path}`,
787
+ );
788
+ }
789
+ return {
790
+ manifest: JSON.parse(bytes.toString('utf8')),
791
+ recommendation,
792
+ context_manifest_path: existingPath,
793
+ manifest_hash: existingHash,
794
+ };
795
+ }
796
+ const manifest = {
797
+ schema_version: semanticManifest.schema_version,
798
+ snapshot_id: snapshotId,
799
+ generated_at: nowIso(),
800
+ ...Object.fromEntries(Object.entries(semanticManifest).filter(([key]) => key !== 'schema_version')),
801
+ };
737
802
  const artifactDir = path.resolve(rootDir, change.artifact_root);
738
803
  fs.mkdirSync(artifactDir, { recursive: true });
739
804
  const manifestPath = path.join(artifactDir, 'contexts', `${snapshotId}.json`);
@@ -796,7 +861,10 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
796
861
  };
797
862
  }
798
863
 
799
- function createChange(db, input, { rootDir = process.cwd() } = {}) {
864
+ function createChange(db, input, {
865
+ rootDir = process.cwd(),
866
+ kernelMode = false,
867
+ } = {}) {
800
868
  const title = typeof input?.title === 'string' ? input.title.trim() : '';
801
869
  const intent = typeof input?.intent === 'string' ? input.intent.trim() : '';
802
870
  if (!input || !CHANGE_ID.test(input.id || '') || title.length < 3 || intent.length < 3) {
@@ -816,7 +884,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
816
884
  if (readChange(db, input.id)) throw new ChangeWorkflowError('DUPLICATE_CHANGE_ID', `change ${input.id} exists`);
817
885
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
818
886
  let baselineBypass = null;
819
- if (baselineHealth.status !== 'pass') {
887
+ if (baselineHealth.status !== 'pass' && !kernelMode) {
820
888
  if (input.kind !== 'incident') {
821
889
  throw new ChangeWorkflowError(
822
890
  'BASELINE_NOT_READY',
@@ -830,7 +898,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
830
898
  'VALIDATION_ERROR', 'baseline_bypass is not valid while the project baseline is healthy',
831
899
  );
832
900
  }
833
- const alignmentGate = decisions.decisionGate(
901
+ const alignmentGate = kernelMode ? { ready: true, blockers: [] } : legacyDecisions().decisionGate(
834
902
  db, { baseline_id: baselineHealth.baseline?.id || null }, { rootDir },
835
903
  );
836
904
  if (!alignmentGate.ready) {
@@ -848,7 +916,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
848
916
  let alignmentThread = null;
849
917
  if (input.alignment_thread_id !== undefined) {
850
918
  try {
851
- alignmentThread = decisions.assertConfirmedDecisionCheckpoint(
919
+ alignmentThread = legacyDecisions().assertConfirmedDecisionCheckpoint(
852
920
  db, input.alignment_thread_id, { rootDir, requireArtifact: true },
853
921
  );
854
922
  } catch (error) {
@@ -920,7 +988,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
920
988
  alignment_thread_id: row.alignment_thread_id,
921
989
  },
922
990
  });
923
- let workflow = workflows.startWorkflow(db, {
991
+ let workflow = kernelMode ? null : legacyWorkflows().startWorkflow(db, {
924
992
  kind: 'change', baseline_id: baselineHealth.baseline?.id || null,
925
993
  change_id: row.id, subject: `Establish executable readiness for ${row.title}.`,
926
994
  metadata: { change_kind: row.kind, base_commit: row.base_commit },
@@ -928,7 +996,11 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
928
996
  upsertArtifact(db, {
929
997
  change_id: row.id, kind: 'intent', artifactPath: path.relative(rootDir, intentPath),
930
998
  contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
931
- workflow_id: workflow.id,
999
+ workflow_id: workflow?.id || null,
1000
+ consumer_ref: kernelMode
1001
+ ? { type: 'external', id: 'ultra-plan', relation: 'consumed_by' }
1002
+ : null,
1003
+ writer: kernelMode ? 'ultra.record' : 'change-workflow',
932
1004
  rootDir,
933
1005
  });
934
1006
  if (row.kind === 'incident') {
@@ -939,11 +1011,29 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
939
1011
  artifactPath: path.relative(rootDir, diagnosisPath),
940
1012
  contentHash: crypto.createHash('sha256').update(fs.readFileSync(diagnosisPath)).digest('hex'),
941
1013
  metadata: { required_sections: INCIDENT_DIAGNOSIS_SECTIONS.map(({ key }) => key) },
942
- workflow_id: workflow.id,
1014
+ workflow_id: workflow?.id || null,
1015
+ consumer_ref: kernelMode
1016
+ ? { type: 'external', id: 'ultra-dev', relation: 'consumed_by' }
1017
+ : null,
1018
+ writer: kernelMode ? 'ultra.record' : 'change-workflow',
943
1019
  rootDir,
944
1020
  });
945
1021
  }
946
- workflow = workflows.recordWorkflowStep(db, {
1022
+ if (kernelMode) {
1023
+ return {
1024
+ change: readChange(db, row.id),
1025
+ intent_path: intentPath,
1026
+ context_manifest_path: null,
1027
+ workflow: null,
1028
+ diagnostics: baselineHealth.status === 'pass'
1029
+ ? []
1030
+ : (baselineHealth.blockers || []).map((code) => ({
1031
+ code,
1032
+ severity: 'needs_attention',
1033
+ })),
1034
+ };
1035
+ }
1036
+ workflow = legacyWorkflows().recordWorkflowStep(db, {
947
1037
  id: workflow.id, step_id: 'bind-baseline', status: 'completed',
948
1038
  evidence: [{
949
1039
  kind: 'baseline', ref: baselineHealth.baseline?.id || 'incident-break-glass',
@@ -952,14 +1042,14 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
952
1042
  : 'Bound the approved incident break-glass authority.',
953
1043
  }],
954
1044
  }, { rootDir });
955
- workflow = workflows.recordWorkflowStep(db, {
1045
+ workflow = legacyWorkflows().recordWorkflowStep(db, {
956
1046
  id: workflow.id, step_id: 'classify-change', status: 'completed',
957
1047
  decisions: [{
958
1048
  kind: 'change_kind', value: row.kind,
959
1049
  rationale: row.classification.rationale, risk_flags: row.classification.risk_flags,
960
1050
  }],
961
1051
  }, { rootDir });
962
- workflow = workflows.recordWorkflowStep(db, {
1052
+ workflow = legacyWorkflows().recordWorkflowStep(db, {
963
1053
  id: workflow.id, step_id: 'record-intent', status: 'completed',
964
1054
  evidence: [{ kind: 'intent', ref: path.relative(rootDir, intentPath), summary: row.intent }],
965
1055
  outputs: [{ path: path.relative(rootDir, intentPath), kind: 'change-intent' }],
@@ -968,7 +1058,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
968
1058
  research_disposition: row.research_disposition,
969
1059
  }],
970
1060
  }, { rootDir });
971
- workflow = workflows.completeWorkflow(db, { id: workflow.id }, { rootDir });
1061
+ workflow = legacyWorkflows().completeWorkflow(db, { id: workflow.id }, { rootDir });
972
1062
  return {
973
1063
  change: readChange(db, row.id), intent_path: intentPath,
974
1064
  context_manifest_path: null,
@@ -992,7 +1082,7 @@ function assertTaskCreationAllowed(db, input = {}, { rootDir = process.cwd() } =
992
1082
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
993
1083
  if (baselineHealth.status === 'pass') return;
994
1084
  const gate = change
995
- ? contextSpine.baselineGateForChange(db, change, baselineHealth)
1085
+ ? legacyContextSpine().baselineGateForChange(db, change, baselineHealth)
996
1086
  : { blockers: baselineHealth.blockers };
997
1087
  if (gate.blockers.length === 0) return;
998
1088
  throw new ChangeWorkflowError(
@@ -1002,10 +1092,13 @@ function assertTaskCreationAllowed(db, input = {}, { rootDir = process.cwd() } =
1002
1092
  );
1003
1093
  }
1004
1094
 
1005
- function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1095
+ function updateChange(db, id, patch = {}, {
1096
+ rootDir = process.cwd(),
1097
+ kernelMode = false,
1098
+ } = {}) {
1006
1099
  const current = readChange(db, id);
1007
1100
  if (!current) throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${id} not found`);
1008
- if (['ready', 'archived', 'cancelled'].includes(current.status)) {
1101
+ if (['archived', 'cancelled'].includes(current.status)) {
1009
1102
  throw new ChangeWorkflowError('CHANGE_NOT_MUTABLE', `change ${id} is ${current.status}`);
1010
1103
  }
1011
1104
  for (const field of Object.keys(patch)) {
@@ -1013,6 +1106,13 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1013
1106
  throw new ChangeWorkflowError('VALIDATION_ERROR', `change field ${field} is not patchable`);
1014
1107
  }
1015
1108
  }
1109
+ if (kernelMode && patch.status !== undefined
1110
+ && !['active', 'cancelled'].includes(patch.status)) {
1111
+ throw new ChangeWorkflowError(
1112
+ 'VALIDATION_ERROR',
1113
+ 'kernel Change status may only be active or cancelled; readiness is derived from checkpoints',
1114
+ );
1115
+ }
1016
1116
  const syncIntent = [
1017
1117
  'title', 'intent', 'docs_impact', 'contract', 'classification', 'research_disposition',
1018
1118
  ].some((field) => patch[field] !== undefined);
@@ -1052,13 +1152,18 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1052
1152
  values.push(JSON.stringify(normalizeResearchDisposition(patch.research_disposition, current.kind)));
1053
1153
  }
1054
1154
  if (patch.status !== undefined) {
1055
- const allowed = CHANGE_TRANSITIONS[current.status] || new Set();
1155
+ const allowed = kernelMode
1156
+ ? new Set(['active', 'cancelled'])
1157
+ : (CHANGE_TRANSITIONS[current.status] || new Set());
1056
1158
  if (!allowed.has(patch.status)) {
1057
1159
  throw new ChangeWorkflowError(
1058
1160
  'ILLEGAL_CHANGE_TRANSITION', `cannot transition change ${id} from ${current.status} to ${patch.status}`,
1059
1161
  );
1060
1162
  }
1061
1163
  sets.push('status = ?'); values.push(patch.status);
1164
+ } else if (['ready', 'blocked'].includes(current.status)
1165
+ && Object.keys(patch).length > 0) {
1166
+ sets.push('status = ?'); values.push('active');
1062
1167
  }
1063
1168
  if (sets.length === 0) return current;
1064
1169
  sets.push('updated_at = ?'); values.push(nowIso(), id);
@@ -1074,6 +1179,10 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1074
1179
  kind: 'intent',
1075
1180
  artifactPath: path.relative(rootDir, intentPath),
1076
1181
  contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
1182
+ consumer_ref: kernelMode
1183
+ ? { type: 'external', id: 'ultra-plan', relation: 'consumed_by' }
1184
+ : null,
1185
+ writer: kernelMode ? 'ultra.record' : 'change-workflow',
1077
1186
  rootDir,
1078
1187
  });
1079
1188
  intentArtifactId = recorded.artifact.id;
@@ -1082,7 +1191,8 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1082
1191
  const semanticFields = Object.keys(patch)
1083
1192
  .filter((field) => SEMANTIC_AUTHORITY_FIELDS.has(field));
1084
1193
  if (semanticFields.length > 0
1085
- && contextSpine.changeStateDigest(current) !== contextSpine.changeStateDigest(updated)) {
1194
+ && legacyContextSpine().changeStateDigest(current)
1195
+ !== legacyContextSpine().changeStateDigest(updated)) {
1086
1196
  invalidated.push(...artifacts.invalidateConsumersFromEndpointInTx(
1087
1197
  db,
1088
1198
  { type: 'change', id },
@@ -1246,7 +1356,7 @@ function generatePacketReconciliation(db, change, packet, rootDir) {
1246
1356
 
1247
1357
  function baselineConvergenceBlockers(db, change, rootDir) {
1248
1358
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
1249
- return contextSpine.baselineGateForChange(db, change, baselineHealth).blockers;
1359
+ return legacyContextSpine().baselineGateForChange(db, change, baselineHealth).blockers;
1250
1360
  }
1251
1361
 
1252
1362
  function deriveConvergenceEvidence(db, change, tasks, workflowGate, diagnosis, rootDir) {
@@ -1255,7 +1365,7 @@ function deriveConvergenceEvidence(db, change, tasks, workflowGate, diagnosis, r
1255
1365
  WHERE kind = 'dev' AND change_id = ? AND status = 'completed'
1256
1366
  ORDER BY completed_at ASC, rowid ASC`,
1257
1367
  ).all(change.id)
1258
- .filter((row) => workflows.isConsumableWorkflowAuthority({
1368
+ .filter((row) => legacyWorkflows().isConsumableWorkflowAuthority({
1259
1369
  metadata: parseJson(row.metadata_json, 'workflow_runs.metadata_json'),
1260
1370
  }))
1261
1371
  .map((row) => ({
@@ -1266,7 +1376,7 @@ function deriveConvergenceEvidence(db, change, tasks, workflowGate, diagnosis, r
1266
1376
  const deltaPaths = deltaAuthority
1267
1377
  ? [deltaAuthority.path, deltaAuthority.digest]
1268
1378
  : [];
1269
- const appliedLearning = specLearning.listSpecLearning(db, change.id)
1379
+ const appliedLearning = legacySpecLearning().listSpecLearning(db, change.id)
1270
1380
  .filter((candidate) => candidate.status === 'applied')
1271
1381
  .map((candidate) => candidate.target_ref);
1272
1382
  const rows = [
@@ -1328,7 +1438,11 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
1328
1438
  const blockers = new Set();
1329
1439
  let workflowGate = null;
1330
1440
  try {
1331
- workflowGate = workflows.validateDeliveryPrerequisites(db, { change_id: change.id }, rootDir);
1441
+ workflowGate = legacyWorkflows().validateDeliveryPrerequisites(
1442
+ db,
1443
+ { change_id: change.id },
1444
+ rootDir,
1445
+ );
1332
1446
  } catch (error) {
1333
1447
  blockers.add(error.code || 'WORKFLOW_GATE_UNAVAILABLE');
1334
1448
  }
@@ -1337,7 +1451,7 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
1337
1451
  if (tasks.some((task) => !['completed', 'expanded'].includes(task.status))) blockers.add('TASKS_INCOMPLETE');
1338
1452
  if (tasks.some((task) => Boolean(task.stale))) blockers.add('TASK_CONTEXT_STALE');
1339
1453
  for (const task of tasks.filter((item) => item.status !== 'expanded')) {
1340
- const validation = contextSpine.validateContextSnapshot(db, {
1454
+ const validation = legacyContextSpine().validateContextSnapshot(db, {
1341
1455
  change_id: change.id,
1342
1456
  task_id: task.id,
1343
1457
  role: 'implement',
@@ -1358,7 +1472,7 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
1358
1472
  }
1359
1473
  }
1360
1474
 
1361
- for (const candidate of specLearning.listSpecLearning(db, change.id)) {
1475
+ for (const candidate of legacySpecLearning().listSpecLearning(db, change.id)) {
1362
1476
  if (candidate.status === 'proposed') blockers.add(`SPEC_LEARNING_UNRESOLVED:${candidate.id}`);
1363
1477
  if (candidate.status === 'approved') blockers.add(`SPEC_LEARNING_NOT_APPLIED:${candidate.id}`);
1364
1478
  }
@@ -1475,6 +1589,131 @@ function replaceArchiveAuthorityText(
1475
1589
  return result;
1476
1590
  }
1477
1591
 
1592
+ function kernelArchiveContextPath(change, id) {
1593
+ return `${change.artifact_root}/authority/context-envelopes/${id}.json`;
1594
+ }
1595
+
1596
+ function kernelArchiveWorkerPath(change, id) {
1597
+ return `${change.artifact_root}/authority/worker-packets/${id}.json`;
1598
+ }
1599
+
1600
+ function materializeKernelArchiveAuthority(db, change, rootDir) {
1601
+ const taskIds = db.prepare(
1602
+ 'SELECT id FROM tasks WHERE change_id = ? ORDER BY id',
1603
+ ).all(change.id).map((row) => row.id);
1604
+ const contexts = db.prepare(
1605
+ `SELECT * FROM context_envelopes
1606
+ WHERE (scope_type = 'change' AND scope_id = ?)
1607
+ OR (scope_type = 'task' AND scope_id IN (
1608
+ SELECT id FROM tasks WHERE change_id = ?
1609
+ ))
1610
+ ORDER BY id`,
1611
+ ).all(change.id, change.id);
1612
+ for (const row of contexts) {
1613
+ const read = readStableProjectFile(rootDir, row.artifact_path);
1614
+ if (!row.file_digest || read.digest !== row.file_digest) {
1615
+ throw new ChangeWorkflowError(
1616
+ 'CONTEXT_ENVELOPE_FILE_DRIFT',
1617
+ `cannot archive unbound Context Envelope ${row.id}`,
1618
+ {
1619
+ path: row.artifact_path,
1620
+ expected: row.file_digest || null,
1621
+ actual: read.digest,
1622
+ },
1623
+ );
1624
+ }
1625
+ const target = kernelArchiveContextPath(change, row.id);
1626
+ const published = writeManagedFile(rootDir, target, read.bytes);
1627
+ upsertArtifact(db, {
1628
+ change_id: change.id,
1629
+ task_id: row.scope_type === 'task' ? row.scope_id : null,
1630
+ kind: 'archive_context_envelope',
1631
+ artifactPath: target,
1632
+ contentHash: published.digest,
1633
+ consumer_ref: {
1634
+ type: 'external',
1635
+ id: 'ultra.archive',
1636
+ relation: 'archived_by',
1637
+ },
1638
+ writer: 'ultra.archive',
1639
+ rootDir,
1640
+ });
1641
+ }
1642
+ if (taskIds.length === 0) return;
1643
+ const packets = db.prepare(
1644
+ `SELECT * FROM worker_packets
1645
+ WHERE scope_type = 'task' AND status = 'assigned'
1646
+ AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)
1647
+ ORDER BY id`,
1648
+ ).all(change.id);
1649
+ for (const row of packets) {
1650
+ const read = readStableProjectFile(rootDir, row.packet_path);
1651
+ if (!row.file_digest || read.digest !== row.file_digest) {
1652
+ throw new ChangeWorkflowError(
1653
+ 'WORKER_PACKET_FILE_DRIFT',
1654
+ `cannot archive unbound Worker Packet ${row.id}`,
1655
+ {
1656
+ path: row.packet_path,
1657
+ expected: row.file_digest || null,
1658
+ actual: read.digest,
1659
+ },
1660
+ );
1661
+ }
1662
+ const target = kernelArchiveWorkerPath(change, row.id);
1663
+ const published = writeManagedFile(rootDir, target, read.bytes);
1664
+ upsertArtifact(db, {
1665
+ change_id: change.id,
1666
+ task_id: row.scope_id,
1667
+ kind: 'archive_worker_packet',
1668
+ artifactPath: target,
1669
+ contentHash: published.digest,
1670
+ consumer_ref: {
1671
+ type: 'external',
1672
+ id: 'ultra.archive',
1673
+ relation: 'archived_by',
1674
+ },
1675
+ writer: 'ultra.archive',
1676
+ rootDir,
1677
+ });
1678
+ }
1679
+ }
1680
+
1681
+ function normalizedArchiveDocument(bytes, intent) {
1682
+ const text = bytes.toString('utf8');
1683
+ if (!Buffer.from(text, 'utf8').equals(bytes)) return { bytes, semanticDigest: null };
1684
+ let document;
1685
+ try { document = JSON.parse(text); }
1686
+ catch { return { bytes, semanticDigest: null }; }
1687
+ let semanticDigest = null;
1688
+ if (document?.schema_version === '1.0'
1689
+ && document.detail === 'full'
1690
+ && document.envelope
1691
+ && document.digest) {
1692
+ if (document.envelope.change?.id === intent.change_id) {
1693
+ document.envelope.change.status = 'archived';
1694
+ }
1695
+ semanticDigest = canonical.digest(document.envelope);
1696
+ document.digest = semanticDigest;
1697
+ } else if (document?.packet_version && document.packet_digest) {
1698
+ const { packet_digest: _prior, ...value } = document;
1699
+ semanticDigest = canonical.digest(value);
1700
+ document.packet_digest = semanticDigest;
1701
+ } else if (document?.schema_version === '1.0'
1702
+ && document.question
1703
+ && document.selection
1704
+ && document.digest) {
1705
+ const { digest: _prior, ...value } = document;
1706
+ semanticDigest = canonical.digest(value);
1707
+ document.digest = semanticDigest;
1708
+ } else if (document?.change?.id === intent.change_id) {
1709
+ document.change.status = 'archived';
1710
+ }
1711
+ return {
1712
+ bytes: Buffer.from(`${JSON.stringify(document, null, 2)}\n`),
1713
+ semanticDigest,
1714
+ };
1715
+ }
1716
+
1478
1717
  function archiveRebindAuthorityValue(value, source, destination, digestMap) {
1479
1718
  if (typeof value === 'string') {
1480
1719
  return replaceArchiveAuthorityText(
@@ -1510,6 +1749,7 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1510
1749
  );
1511
1750
  }
1512
1751
  const originalByPath = new Map();
1752
+ const semanticByPath = new Map();
1513
1753
  for (const artifact of db.prepare(
1514
1754
  'SELECT path, digest, content_hash FROM artifacts WHERE change_id = ?',
1515
1755
  ).all(intent.change_id)) {
@@ -1531,6 +1771,35 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1531
1771
  originalByPath.set(archivedPath, snapshot.manifest_hash);
1532
1772
  }
1533
1773
  }
1774
+ for (const row of db.prepare(
1775
+ `SELECT id, digest FROM decision_records
1776
+ WHERE scope_type = 'change' AND scope_id = ?`,
1777
+ ).all(intent.change_id)) {
1778
+ const archivedPath = `${intent.destination}/decisions/${row.id}.json`;
1779
+ semanticByPath.set(archivedPath, row.digest);
1780
+ }
1781
+ for (const row of db.prepare(
1782
+ `SELECT id, digest FROM context_envelopes
1783
+ WHERE (scope_type = 'change' AND scope_id = ?)
1784
+ OR (scope_type = 'task' AND scope_id IN (
1785
+ SELECT id FROM tasks WHERE change_id = ?
1786
+ ))`,
1787
+ ).all(intent.change_id, intent.change_id)) {
1788
+ semanticByPath.set(
1789
+ `${intent.destination}/authority/context-envelopes/${row.id}.json`,
1790
+ row.digest,
1791
+ );
1792
+ }
1793
+ for (const row of db.prepare(
1794
+ `SELECT id, packet_digest FROM worker_packets
1795
+ WHERE scope_type = 'task' AND status = 'assigned'
1796
+ AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)`,
1797
+ ).all(intent.change_id)) {
1798
+ semanticByPath.set(
1799
+ `${intent.destination}/authority/worker-packets/${row.id}.json`,
1800
+ row.packet_digest,
1801
+ );
1802
+ }
1534
1803
  const original = new Map();
1535
1804
  const pathRebound = new Map();
1536
1805
  for (const relative of tree.files) {
@@ -1567,7 +1836,10 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1567
1836
  manifest.change.status = 'archived';
1568
1837
  next = `${JSON.stringify(manifest, null, 2)}\n`;
1569
1838
  }
1570
- pathRebound.set(relative, Buffer.from(next));
1839
+ pathRebound.set(
1840
+ relative,
1841
+ normalizedArchiveDocument(Buffer.from(next), intent).bytes,
1842
+ );
1571
1843
  }
1572
1844
 
1573
1845
  let current = new Map(pathRebound);
@@ -1582,6 +1854,14 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1582
1854
  );
1583
1855
  }
1584
1856
  }
1857
+ for (const [relative, beforeDigest] of semanticByPath) {
1858
+ const bytes = current.get(relative);
1859
+ if (!bytes) continue;
1860
+ const normalized = normalizedArchiveDocument(bytes, intent);
1861
+ if (normalized.semanticDigest) {
1862
+ digestMap.set(beforeDigest, normalized.semanticDigest);
1863
+ }
1864
+ }
1585
1865
  const next = new Map();
1586
1866
  let changed = false;
1587
1867
  for (const [relative, baseBytes] of pathRebound) {
@@ -1590,11 +1870,12 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1590
1870
  next.set(relative, baseBytes);
1591
1871
  continue;
1592
1872
  }
1593
- const bytes = Buffer.from(
1873
+ const replaced = Buffer.from(
1594
1874
  replaceArchiveAuthorityText(
1595
1875
  text, intent.source, intent.destination, digestMap,
1596
1876
  ),
1597
1877
  );
1878
+ const bytes = normalizedArchiveDocument(replaced, intent).bytes;
1598
1879
  next.set(relative, bytes);
1599
1880
  if (!bytes.equals(current.get(relative))) changed = true;
1600
1881
  }
@@ -1633,10 +1914,52 @@ function archiveRebindState(db, intent, rootDir) {
1633
1914
  const digestMap = new Map(journal.entries.map(
1634
1915
  (entry) => [entry.before_digest, entry.after_digest],
1635
1916
  ));
1917
+ const semanticEntry = (relativePath) => byPath.get(relativePath);
1918
+ for (const row of db.prepare(
1919
+ `SELECT id, digest FROM decision_records
1920
+ WHERE scope_type = 'change' AND scope_id = ?`,
1921
+ ).all(intent.change_id)) {
1922
+ const entry = semanticEntry(`${intent.destination}/decisions/${row.id}.json`);
1923
+ if (!entry) continue;
1924
+ const document = JSON.parse(Buffer.from(entry.after_base64, 'base64').toString('utf8'));
1925
+ if (document.digest) digestMap.set(row.digest, document.digest);
1926
+ }
1927
+ for (const row of db.prepare(
1928
+ `SELECT id, digest FROM context_envelopes
1929
+ WHERE (scope_type = 'change' AND scope_id = ?)
1930
+ OR (scope_type = 'task' AND scope_id IN (
1931
+ SELECT id FROM tasks WHERE change_id = ?
1932
+ ))`,
1933
+ ).all(intent.change_id, intent.change_id)) {
1934
+ const entry = semanticEntry(
1935
+ `${intent.destination}/authority/context-envelopes/${row.id}.json`,
1936
+ );
1937
+ if (!entry) continue;
1938
+ const document = JSON.parse(Buffer.from(entry.after_base64, 'base64').toString('utf8'));
1939
+ if (document.digest) digestMap.set(row.digest, document.digest);
1940
+ }
1941
+ for (const row of db.prepare(
1942
+ `SELECT id, packet_digest FROM worker_packets
1943
+ WHERE scope_type = 'task' AND status = 'assigned'
1944
+ AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)`,
1945
+ ).all(intent.change_id)) {
1946
+ const entry = semanticEntry(
1947
+ `${intent.destination}/authority/worker-packets/${row.id}.json`,
1948
+ );
1949
+ if (!entry) continue;
1950
+ const document = JSON.parse(Buffer.from(entry.after_base64, 'base64').toString('utf8'));
1951
+ if (document.packet_digest) {
1952
+ digestMap.set(row.packet_digest, document.packet_digest);
1953
+ }
1954
+ }
1636
1955
  return { journal, byPath, digestMap };
1637
1956
  }
1638
1957
 
1639
- function finalizeArchive(db, intent, { rootDir, journalIntent = intent }) {
1958
+ function finalizeArchive(db, intent, {
1959
+ rootDir,
1960
+ journalIntent = intent,
1961
+ kernelMode = false,
1962
+ }) {
1640
1963
  const change = readChange(db, intent.change_id);
1641
1964
  if (!change) throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${intent.change_id} not found`);
1642
1965
  try {
@@ -1654,8 +1977,12 @@ function finalizeArchive(db, intent, { rootDir, journalIntent = intent }) {
1654
1977
  && artifacts.normalizeRelativePath(change.artifact_root) === relative) {
1655
1978
  return { change, archive_path: destination };
1656
1979
  }
1657
- if (change.status !== 'ready') {
1658
- throw new ChangeWorkflowError('CHANGE_NOT_READY', `change ${change.id} must be ready to finish archive`);
1980
+ const acceptedStatuses = kernelMode ? ['active', 'ready'] : ['ready'];
1981
+ if (!acceptedStatuses.includes(change.status)) {
1982
+ throw new ChangeWorkflowError(
1983
+ 'CHANGE_NOT_READY',
1984
+ `change ${change.id} must have complete accepted checkpoints to finish archive`,
1985
+ );
1659
1986
  }
1660
1987
  const rebind = archiveRebindState(db, journalIntent, rootDir);
1661
1988
  const archivedReconciliationPath = rebindRegistryPath(
@@ -1800,6 +2127,210 @@ function finalizeArchive(db, intent, { rootDir, journalIntent = intent }) {
1800
2127
  snapshot.id,
1801
2128
  );
1802
2129
  }
2130
+ for (const decision of db.prepare(
2131
+ `SELECT id FROM decision_records
2132
+ WHERE scope_type = 'change' AND scope_id = ?`,
2133
+ ).all(change.id)) {
2134
+ const archivedPath = `${relative}/decisions/${decision.id}.json`;
2135
+ const rebound = rebind.byPath.get(archivedPath);
2136
+ if (!rebound) {
2137
+ throw new ChangeWorkflowError(
2138
+ 'ARCHIVE_DECISION_MISSING',
2139
+ `archive is missing Decision Record ${decision.id}`,
2140
+ );
2141
+ }
2142
+ const document = JSON.parse(
2143
+ Buffer.from(rebound.after_base64, 'base64').toString('utf8'),
2144
+ );
2145
+ db.prepare(
2146
+ `UPDATE decision_records
2147
+ SET question = ?, recommendation = ?, selection = ?,
2148
+ effects_json = ?, non_goals_json = ?, owner = ?, source = ?,
2149
+ provenance_json = ?, applied_refs_json = ?, digest = ?,
2150
+ artifact_path = ?, updated_at = ?
2151
+ WHERE id = ?`,
2152
+ ).run(
2153
+ document.question,
2154
+ document.recommendation,
2155
+ document.selection,
2156
+ JSON.stringify(document.effects || {}),
2157
+ JSON.stringify(document.non_goals || []),
2158
+ document.owner,
2159
+ document.source,
2160
+ JSON.stringify(document.provenance || {}),
2161
+ JSON.stringify(document.applied_refs || []),
2162
+ document.digest,
2163
+ archivedPath,
2164
+ nowIso(),
2165
+ decision.id,
2166
+ );
2167
+ }
2168
+ for (const envelope of db.prepare(
2169
+ `SELECT id FROM context_envelopes
2170
+ WHERE (scope_type = 'change' AND scope_id = ?)
2171
+ OR (scope_type = 'task' AND scope_id IN (
2172
+ SELECT id FROM tasks WHERE change_id = ?
2173
+ ))`,
2174
+ ).all(change.id, change.id)) {
2175
+ const archivedPath = `${relative}/authority/context-envelopes/${envelope.id}.json`;
2176
+ const rebound = rebind.byPath.get(archivedPath);
2177
+ if (!rebound) {
2178
+ throw new ChangeWorkflowError(
2179
+ 'ARCHIVE_CONTEXT_MISSING',
2180
+ `archive is missing Context Envelope ${envelope.id}`,
2181
+ );
2182
+ }
2183
+ const document = JSON.parse(
2184
+ Buffer.from(rebound.after_base64, 'base64').toString('utf8'),
2185
+ );
2186
+ db.prepare(
2187
+ `UPDATE context_envelopes
2188
+ SET digest = ?, file_digest = ?, payload_json = ?, artifact_path = ?
2189
+ WHERE id = ?`,
2190
+ ).run(
2191
+ document.digest,
2192
+ rebound.after_digest,
2193
+ JSON.stringify(document.envelope),
2194
+ archivedPath,
2195
+ envelope.id,
2196
+ );
2197
+ }
2198
+ for (const packet of db.prepare(
2199
+ `SELECT id FROM worker_packets
2200
+ WHERE scope_type = 'task' AND status = 'assigned'
2201
+ AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)`,
2202
+ ).all(change.id)) {
2203
+ const archivedPath = `${relative}/authority/worker-packets/${packet.id}.json`;
2204
+ const rebound = rebind.byPath.get(archivedPath);
2205
+ if (!rebound) {
2206
+ throw new ChangeWorkflowError(
2207
+ 'ARCHIVE_WORKER_PACKET_MISSING',
2208
+ `archive is missing Worker Packet ${packet.id}`,
2209
+ );
2210
+ }
2211
+ const document = JSON.parse(
2212
+ Buffer.from(rebound.after_base64, 'base64').toString('utf8'),
2213
+ );
2214
+ const decisionDigest = canonical.digest(
2215
+ (document.accepted_decisions || []).map((decision) => ({
2216
+ id: decision.id,
2217
+ digest: decision.digest,
2218
+ status: 'accepted',
2219
+ })),
2220
+ );
2221
+ db.prepare(
2222
+ `UPDATE worker_packets
2223
+ SET context_digest = ?, task_digest = ?, decision_digest = ?,
2224
+ packet_digest = ?, file_digest = ?, packet_path = ?, output_path = ?
2225
+ WHERE id = ?`,
2226
+ ).run(
2227
+ document.context_envelope?.digest || null,
2228
+ document.task?.digest || null,
2229
+ decisionDigest,
2230
+ document.packet_digest,
2231
+ rebound.after_digest,
2232
+ archivedPath,
2233
+ document.output?.path,
2234
+ packet.id,
2235
+ );
2236
+ }
2237
+ const checkpointRows = db.prepare(
2238
+ `SELECT *
2239
+ FROM stage_checkpoints
2240
+ WHERE (scope_type = 'change' AND scope_id = ?)
2241
+ OR (scope_type = 'task' AND scope_id IN (
2242
+ SELECT id FROM tasks WHERE change_id = ?
2243
+ ))
2244
+ ORDER BY revision`,
2245
+ ).all(change.id, change.id);
2246
+ for (const row of checkpointRows) {
2247
+ const value = {
2248
+ stage: row.stage,
2249
+ scope_type: row.scope_type,
2250
+ scope_id: row.scope_id,
2251
+ revision: row.revision,
2252
+ payload: archiveRebindAuthorityValue(
2253
+ parseJson(row.payload_json || '{}', 'stage_checkpoints.payload_json'),
2254
+ intent.source,
2255
+ relative,
2256
+ rebind.digestMap,
2257
+ ),
2258
+ evidence: archiveRebindAuthorityValue(
2259
+ parseJson(row.evidence_json || '[]', 'stage_checkpoints.evidence_json'),
2260
+ intent.source,
2261
+ relative,
2262
+ rebind.digestMap,
2263
+ ),
2264
+ diagnostics: archiveRebindAuthorityValue(
2265
+ parseJson(row.diagnostics_json || '[]', 'stage_checkpoints.diagnostics_json'),
2266
+ intent.source,
2267
+ relative,
2268
+ rebind.digestMap,
2269
+ ),
2270
+ context_envelope_id: row.context_envelope_id,
2271
+ supersedes_id: row.supersedes_id,
2272
+ };
2273
+ db.prepare(
2274
+ `UPDATE stage_checkpoints
2275
+ SET payload_json = ?, evidence_json = ?, diagnostics_json = ?, digest = ?,
2276
+ updated_at = ?
2277
+ WHERE id = ?`,
2278
+ ).run(
2279
+ JSON.stringify(value.payload),
2280
+ JSON.stringify(value.evidence),
2281
+ JSON.stringify(value.diagnostics),
2282
+ stageCheckpoints.checkpointDigest(value),
2283
+ nowIso(),
2284
+ row.id,
2285
+ );
2286
+ }
2287
+ for (const task of db.prepare(
2288
+ `SELECT id, context_refs_json, docs_impact_json, ownership_json
2289
+ FROM tasks WHERE change_id = ?`,
2290
+ ).all(change.id)) {
2291
+ db.prepare(
2292
+ `UPDATE tasks
2293
+ SET context_refs_json = ?, docs_impact_json = ?, ownership_json = ?,
2294
+ updated_at = ?
2295
+ WHERE id = ?`,
2296
+ ).run(
2297
+ JSON.stringify(archiveRebindAuthorityValue(
2298
+ parseJson(task.context_refs_json || '[]', 'tasks.context_refs_json'),
2299
+ intent.source,
2300
+ relative,
2301
+ rebind.digestMap,
2302
+ )),
2303
+ JSON.stringify(archiveRebindAuthorityValue(
2304
+ parseJson(task.docs_impact_json || '{}', 'tasks.docs_impact_json'),
2305
+ intent.source,
2306
+ relative,
2307
+ rebind.digestMap,
2308
+ )),
2309
+ JSON.stringify(archiveRebindAuthorityValue(
2310
+ parseJson(task.ownership_json || '{}', 'tasks.ownership_json'),
2311
+ intent.source,
2312
+ relative,
2313
+ rebind.digestMap,
2314
+ )),
2315
+ nowIso(),
2316
+ task.id,
2317
+ );
2318
+ }
2319
+ for (const event of db.prepare(
2320
+ `SELECT id, payload_json FROM events
2321
+ WHERE change_id = ?
2322
+ OR task_id IN (SELECT id FROM tasks WHERE change_id = ?)`,
2323
+ ).all(change.id, change.id)) {
2324
+ db.prepare('UPDATE events SET payload_json = ? WHERE id = ?').run(
2325
+ JSON.stringify(archiveRebindAuthorityValue(
2326
+ parseJson(event.payload_json || '{}', 'events.payload_json'),
2327
+ intent.source,
2328
+ relative,
2329
+ rebind.digestMap,
2330
+ )),
2331
+ event.id,
2332
+ );
2333
+ }
1803
2334
  for (const run of db.prepare(
1804
2335
  `SELECT id, metadata_json, blockers_json, summary_json, approval_json
1805
2336
  FROM workflow_runs WHERE change_id = ?`,
@@ -1928,13 +2459,19 @@ function finalizeArchive(db, intent, { rootDir, journalIntent = intent }) {
1928
2459
  };
1929
2460
  }
1930
2461
 
1931
- function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
2462
+ function archiveChange(db, input, {
2463
+ rootDir = process.cwd(),
2464
+ kernelMode = false,
2465
+ } = {}) {
1932
2466
  const change = readChange(db, input.id);
1933
2467
  if (!change) throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${input.id} not found`);
1934
2468
  if (!input.summary || String(input.summary).trim().length < 3) {
1935
2469
  throw new ChangeWorkflowError('VALIDATION_ERROR', 'archive summary required');
1936
2470
  }
1937
- if (!['ready', 'archived'].includes(change.status)) {
2471
+ const allowedArchiveStatuses = kernelMode
2472
+ ? ['active', 'archived']
2473
+ : ['ready', 'archived'];
2474
+ if (!allowedArchiveStatuses.includes(change.status)) {
1938
2475
  throw new ChangeWorkflowError('CHANGE_NOT_READY', `change ${input.id} must converge before archive`);
1939
2476
  }
1940
2477
  if (change.status === 'archived') {
@@ -1950,8 +2487,43 @@ function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
1950
2487
  }
1951
2488
 
1952
2489
  let packet = null;
2490
+ if (kernelMode) {
2491
+ const requiredStages = ['plan', 'test', 'review', 'deliver'];
2492
+ const missing = requiredStages.filter((stage) => !stageCheckpoints.currentCheckpoint(
2493
+ db,
2494
+ stage,
2495
+ { change_id: change.id },
2496
+ { includeDraft: false },
2497
+ ));
2498
+ const tasks = ops.listTasks(db, { change_id: change.id });
2499
+ if (tasks.length === 0) missing.push('tasks');
2500
+ for (const task of tasks.filter((item) => item.status !== 'expanded')) {
2501
+ if (task.status !== 'completed') missing.push(`task:${task.id}:completed`);
2502
+ if (!stageCheckpoints.currentCheckpoint(
2503
+ db,
2504
+ 'dev',
2505
+ { task_id: task.id },
2506
+ { includeDraft: false },
2507
+ )) {
2508
+ missing.push(`task:${task.id}:dev`);
2509
+ }
2510
+ }
2511
+ if (missing.length > 0) {
2512
+ throw new ChangeWorkflowError(
2513
+ 'CHANGE_EVIDENCE_INCOMPLETE',
2514
+ `change ${change.id} lacks accepted delivery checkpoints`,
2515
+ { missing },
2516
+ );
2517
+ }
2518
+ }
1953
2519
  if (changePacket.readDeltaAuthority(db, change.id)) {
1954
- workflows.validateDeliveryPrerequisites(db, { change_id: change.id }, rootDir);
2520
+ if (!kernelMode) {
2521
+ legacyWorkflows().validateDeliveryPrerequisites(
2522
+ db,
2523
+ { change_id: change.id },
2524
+ rootDir,
2525
+ );
2526
+ }
1955
2527
  packet = changePacket.deliveryEntries(db, change, { rootDir });
1956
2528
  }
1957
2529
  const requestedUpdates = Array.isArray(input.baseline_updates)
@@ -1982,6 +2554,9 @@ function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
1982
2554
  const reconciliation = packet
1983
2555
  ? generatePacketReconciliation(db, change, packet, rootDir)
1984
2556
  : readReconciliationManifest(db, change, input, rootDir);
2557
+ if (kernelMode) {
2558
+ materializeKernelArchiveAuthority(db, change, rootDir);
2559
+ }
1985
2560
  let delivery = null;
1986
2561
  let prepared = null;
1987
2562
  try {
@@ -2034,6 +2609,7 @@ function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
2034
2609
  result = finalizeArchive(db, authoritativeIntent, {
2035
2610
  rootDir,
2036
2611
  journalIntent: prepared.intent,
2612
+ kernelMode,
2037
2613
  });
2038
2614
  } catch (error) {
2039
2615
  try { archiveJournal.rollbackArchiveIntent(rootDir, prepared.intent); }
@@ -2090,7 +2666,7 @@ function recoverInterruptedArchives(db, { rootDir = process.cwd() } = {}) {
2090
2666
  archiveJournal.completeArchiveIntent(rootDir, record.intent);
2091
2667
  result.cleaned += 1;
2092
2668
  result.items.push({ change_id: change.id, status: 'cleaned' });
2093
- } else if (change?.status === 'ready' && record.location === 'destination') {
2669
+ } else if (['active', 'ready'].includes(change?.status) && record.location === 'destination') {
2094
2670
  const reconciliation = readReconciliationManifest(db, change, {
2095
2671
  reconciliation_path: record.intent.reconciliation_path,
2096
2672
  baseline_updates: record.intent.baseline_updates,
@@ -2103,6 +2679,10 @@ function recoverInterruptedArchives(db, { rootDir = process.cwd() } = {}) {
2103
2679
  finalizeArchive(db, authoritativeIntent, {
2104
2680
  rootDir,
2105
2681
  journalIntent: record.intent,
2682
+ // Recovery is a mechanical continuation of an already prepared
2683
+ // archive intent. It must never re-enter the retired semantic
2684
+ // workflow supervisor, including for a v0.23 `ready` Change.
2685
+ kernelMode: true,
2106
2686
  });
2107
2687
  archiveJournal.completeArchiveIntent(rootDir, record.intent);
2108
2688
  result.resumed += 1;
@@ -2149,10 +2729,18 @@ module.exports = {
2149
2729
  recordDelta,
2150
2730
  recordDocumentationReconciliation,
2151
2731
  compileContext,
2152
- readBreadcrumb: contextSpine.readBreadcrumb,
2153
- proposeSpecLearning: specLearning.proposeSpecLearning,
2154
- resolveSpecLearning: specLearning.resolveSpecLearning,
2155
- listSpecLearning: specLearning.listSpecLearning,
2732
+ readBreadcrumb(...args) {
2733
+ return legacyContextSpine().readBreadcrumb(...args);
2734
+ },
2735
+ proposeSpecLearning(...args) {
2736
+ return legacySpecLearning().proposeSpecLearning(...args);
2737
+ },
2738
+ resolveSpecLearning(...args) {
2739
+ return legacySpecLearning().resolveSpecLearning(...args);
2740
+ },
2741
+ listSpecLearning(...args) {
2742
+ return legacySpecLearning().listSpecLearning(...args);
2743
+ },
2156
2744
  convergeChange,
2157
2745
  archiveChange,
2158
2746
  normalizeDocsImpact,