ultra-builder-pro-cli 0.23.0 → 0.24.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 (95) hide show
  1. package/.ultra-template/docs/research/README.md +4 -4
  2. package/.ultra-template/tasks/tasks.json +4 -2
  3. package/CHANGELOG.md +72 -0
  4. package/README.md +55 -39
  5. package/adapters/_shared/codex-assets.cjs +121 -65
  6. package/adapters/_shared/interaction-contract.cjs +7 -1
  7. package/adapters/_shared/provenance.cjs +7 -2
  8. package/adapters/_shared/runtime-assets.cjs +3 -2
  9. package/adapters/_shared/runtime-native.cjs +132 -0
  10. package/adapters/claude.js +60 -30
  11. package/adapters/codex.js +178 -81
  12. package/adapters/grok.js +372 -0
  13. package/adapters/kimi.js +7 -3
  14. package/adapters/opencode.js +210 -32
  15. package/agents/code-reviewer.md +4 -2
  16. package/agents/debugger.md +4 -1
  17. package/agents/review-code.md +7 -3
  18. package/agents/review-comments.md +7 -4
  19. package/agents/review-coordinator.md +11 -7
  20. package/agents/review-design.md +8 -4
  21. package/agents/review-errors.md +8 -4
  22. package/agents/review-spec.md +8 -5
  23. package/agents/review-tests.md +7 -4
  24. package/agents/tdd-runner.md +5 -2
  25. package/bin/install.js +9 -4
  26. package/bin/orchestrator.js +4 -13
  27. package/docs/AGENT-CONTEXT.md +143 -229
  28. package/docs/ARCHITECTURE.md +205 -494
  29. package/docs/ARTIFACT-AUTHORITY.md +35 -28
  30. package/docs/LEGACY-CLI-CROSSWALK.md +1 -1
  31. package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
  32. package/docs/ROADMAP.md +85 -110
  33. package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
  34. package/docs/SKILL-AUTHORING.md +3 -3
  35. package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
  36. package/docs/WORKFLOW-LIFECYCLE.md +189 -448
  37. package/hooks/README.md +11 -8
  38. package/hooks/active_task_context.py +16 -16
  39. package/hooks/adapters/grok.py +134 -0
  40. package/hooks/{context_spine.py → context_envelope.py} +26 -22
  41. package/hooks/health_check.py +29 -34
  42. package/hooks/pre_stop_check.py +22 -12
  43. package/hooks/subagent_tracker.py +1 -1
  44. package/hooks/workflow_checkpoint.py +14 -13
  45. package/hooks/workflow_context.py +11 -12
  46. package/hooks/workflow_resume.py +14 -13
  47. package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
  48. package/mcp-server/lib/baseline-workflow.cjs +108 -82
  49. package/mcp-server/lib/canonical-json.cjs +19 -0
  50. package/mcp-server/lib/change-authority.cjs +26 -0
  51. package/mcp-server/lib/change-workflow.cjs +630 -45
  52. package/mcp-server/lib/context-envelope.cjs +508 -0
  53. package/mcp-server/lib/decision-records.cjs +337 -0
  54. package/mcp-server/lib/doctor.cjs +91 -33
  55. package/mcp-server/lib/init-project.cjs +78 -39
  56. package/mcp-server/lib/managed-file-write.cjs +116 -0
  57. package/mcp-server/lib/plan-checkpoint.cjs +177 -0
  58. package/mcp-server/lib/project-context.cjs +178 -0
  59. package/mcp-server/lib/stage-checkpoints.cjs +309 -0
  60. package/mcp-server/lib/state-db.cjs +155 -24
  61. package/mcp-server/lib/state-ops.cjs +21 -3
  62. package/mcp-server/lib/task-ledger.cjs +522 -13
  63. package/mcp-server/lib/ultra-facade.cjs +757 -442
  64. package/mcp-server/lib/worker-packet.cjs +358 -0
  65. package/mcp-server/server.cjs +200 -1103
  66. package/orchestrator/daemon.cjs +27 -9
  67. package/orchestrator/parallel-orchestrator.cjs +111 -76
  68. package/orchestrator/session-runner.cjs +135 -34
  69. package/package.json +10 -3
  70. package/skills/ultra-change/SKILL.md +10 -8
  71. package/skills/ultra-deliver/SKILL.md +17 -19
  72. package/skills/ultra-dev/SKILL.md +20 -11
  73. package/skills/ultra-doctor/SKILL.md +4 -4
  74. package/skills/ultra-init/SKILL.md +6 -5
  75. package/skills/ultra-init/references/brownfield-adoption.md +20 -14
  76. package/skills/ultra-plan/SKILL.md +13 -9
  77. package/skills/ultra-research/SKILL.md +18 -9
  78. package/skills/ultra-review/SKILL.md +14 -9
  79. package/skills/ultra-review/references/unified-schema.md +2 -0
  80. package/skills/ultra-review/scripts/review_wait.py +8 -0
  81. package/skills/ultra-status/SKILL.md +2 -2
  82. package/skills/ultra-test/SKILL.md +10 -6
  83. package/skills/ultra-think/SKILL.md +8 -7
  84. package/skills/ultra-think/references/interaction-boundary.md +56 -0
  85. package/spec/cli-protocol.md +80 -199
  86. package/spec/mcp-tools.yaml +43 -2806
  87. package/spec/schemas/state-db.sql +107 -4
  88. package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
  89. package/ultra-tools/commands/status.cjs +369 -243
  90. package/mcp-server/lib/context-spine.cjs +0 -1208
  91. package/mcp-server/lib/decision-dialogue.cjs +0 -1128
  92. package/mcp-server/lib/project-breadcrumb.cjs +0 -173
  93. package/mcp-server/lib/spec-learning.cjs +0 -284
  94. package/mcp-server/lib/workflow-state.cjs +0 -3129
  95. package/skills/ultra-think/references/decision-dialogue.md +0 -155
@@ -8,8 +8,9 @@ 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 changeAuthority = require('./change-authority.cjs');
13
+ const { writeManagedFile } = require('./managed-file-write.cjs');
13
14
  const providerRefs = require('./provider-refs.cjs');
14
15
  const baselines = require('./baseline-workflow.cjs');
15
16
  const archiveJournal = require('./archive-journal.cjs');
@@ -20,13 +21,32 @@ const {
20
21
  readStableProjectFile,
21
22
  walkStableProjectTree,
22
23
  } = require('./safe-project-file.cjs');
23
- const workflows = require('./workflow-state.cjs');
24
- const decisions = require('./decision-dialogue.cjs');
24
+ const stageCheckpoints = require('./stage-checkpoints.cjs');
25
25
  const reconciliationSchema = require('../../spec/schemas/baseline-reconciliation.v1.schema.json');
26
26
 
27
27
  const reconciliationAjv = new Ajv({ allErrors: true, strict: false });
28
28
  const validateReconciliationSchema = reconciliationAjv.compile(reconciliationSchema);
29
29
 
30
+ function loadLegacyModule(filename) {
31
+ return module.require(path.join(__dirname, filename));
32
+ }
33
+
34
+ function legacySpecLearning() {
35
+ return loadLegacyModule('spec-learning.cjs');
36
+ }
37
+
38
+ function legacyWorkflows() {
39
+ return loadLegacyModule('workflow-state.cjs');
40
+ }
41
+
42
+ function legacyDecisions() {
43
+ return loadLegacyModule('decision-dialogue.cjs');
44
+ }
45
+
46
+ function legacyContextSpine() {
47
+ return loadLegacyModule('context-spine.cjs');
48
+ }
49
+
30
50
  const CHANGE_ID = /^[a-zA-Z0-9_-]+$/;
31
51
  const CHANGE_PATCH_FIELDS = new Set([
32
52
  'title', 'intent', 'status', 'docs_impact', 'provider_refs',
@@ -494,7 +514,13 @@ function readReconciliationManifest(db, change, input, rootDir) {
494
514
  );
495
515
  }
496
516
  let resolved;
497
- try { resolved = workflows.resolveProjectSourceRef(rootDir, item.source_ref, 'semantic_change.source_ref'); }
517
+ try {
518
+ resolved = legacyWorkflows().resolveProjectSourceRef(
519
+ rootDir,
520
+ item.source_ref,
521
+ 'semantic_change.source_ref',
522
+ );
523
+ }
498
524
  catch (error) {
499
525
  throw new ChangeWorkflowError('BASELINE_RECONCILIATION_MANIFEST_INVALID', error.message);
500
526
  }
@@ -653,14 +679,17 @@ function upsertArtifact(
653
679
  db,
654
680
  {
655
681
  change_id, task_id = null, kind, artifactPath, contentHash, metadata,
656
- workflow_id = null, terminal = false, rootDir = process.cwd(),
682
+ workflow_id = null, consumer_ref = null, terminal = false,
683
+ writer = 'change-workflow', rootDir = process.cwd(),
657
684
  },
658
685
  ) {
659
686
  const ownerType = task_id ? 'task' : 'change';
660
687
  const ownerId = task_id || change_id;
661
688
  const consumer = terminal
662
689
  ? null
663
- : task_id
690
+ : consumer_ref
691
+ ? consumer_ref
692
+ : task_id
664
693
  ? { type: 'task', id: task_id, relation: 'consumed_by' }
665
694
  : workflow_id
666
695
  ? { type: 'workflow', id: workflow_id, relation: 'consumed_by' }
@@ -674,7 +703,7 @@ function upsertArtifact(
674
703
  source_refs: [{ type: 'change', id: change_id, relation: 'produced_for' }],
675
704
  consumer_refs: consumer ? [consumer] : [],
676
705
  provenance: {
677
- writer: 'change-workflow',
706
+ writer,
678
707
  workflow_run_id: consumer?.type === 'workflow' ? consumer.id : null,
679
708
  },
680
709
  metadata: { ...(metadata || {}), ...(terminal ? { terminal_role: true } : {}) },
@@ -698,7 +727,10 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
698
727
  'SELECT id FROM tasks WHERE change_id = ? ORDER BY created_at ASC',
699
728
  ).all(change.id).map((task) => ops.readTask(db, task.id));
700
729
  const specs = Array.isArray(input.spec_refs) ? input.spec_refs : [];
701
- const spine = contextSpine.compileRoleContext(db, { input, change, tasks, rootDir });
730
+ const spine = legacyContextSpine().compileRoleContext(
731
+ db,
732
+ { input, change, tasks, rootDir },
733
+ );
702
734
  const recommendation = spine.recommendation;
703
735
  const durableSpine = { ...spine };
704
736
  delete durableSpine.recommendation;
@@ -830,7 +862,10 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
830
862
  };
831
863
  }
832
864
 
833
- function createChange(db, input, { rootDir = process.cwd() } = {}) {
865
+ function createChange(db, input, {
866
+ rootDir = process.cwd(),
867
+ kernelMode = false,
868
+ } = {}) {
834
869
  const title = typeof input?.title === 'string' ? input.title.trim() : '';
835
870
  const intent = typeof input?.intent === 'string' ? input.intent.trim() : '';
836
871
  if (!input || !CHANGE_ID.test(input.id || '') || title.length < 3 || intent.length < 3) {
@@ -850,7 +885,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
850
885
  if (readChange(db, input.id)) throw new ChangeWorkflowError('DUPLICATE_CHANGE_ID', `change ${input.id} exists`);
851
886
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
852
887
  let baselineBypass = null;
853
- if (baselineHealth.status !== 'pass') {
888
+ if (baselineHealth.status !== 'pass' && !kernelMode) {
854
889
  if (input.kind !== 'incident') {
855
890
  throw new ChangeWorkflowError(
856
891
  'BASELINE_NOT_READY',
@@ -864,7 +899,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
864
899
  'VALIDATION_ERROR', 'baseline_bypass is not valid while the project baseline is healthy',
865
900
  );
866
901
  }
867
- const alignmentGate = decisions.decisionGate(
902
+ const alignmentGate = kernelMode ? { ready: true, blockers: [] } : legacyDecisions().decisionGate(
868
903
  db, { baseline_id: baselineHealth.baseline?.id || null }, { rootDir },
869
904
  );
870
905
  if (!alignmentGate.ready) {
@@ -882,7 +917,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
882
917
  let alignmentThread = null;
883
918
  if (input.alignment_thread_id !== undefined) {
884
919
  try {
885
- alignmentThread = decisions.assertConfirmedDecisionCheckpoint(
920
+ alignmentThread = legacyDecisions().assertConfirmedDecisionCheckpoint(
886
921
  db, input.alignment_thread_id, { rootDir, requireArtifact: true },
887
922
  );
888
923
  } catch (error) {
@@ -954,7 +989,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
954
989
  alignment_thread_id: row.alignment_thread_id,
955
990
  },
956
991
  });
957
- let workflow = workflows.startWorkflow(db, {
992
+ let workflow = kernelMode ? null : legacyWorkflows().startWorkflow(db, {
958
993
  kind: 'change', baseline_id: baselineHealth.baseline?.id || null,
959
994
  change_id: row.id, subject: `Establish executable readiness for ${row.title}.`,
960
995
  metadata: { change_kind: row.kind, base_commit: row.base_commit },
@@ -962,7 +997,11 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
962
997
  upsertArtifact(db, {
963
998
  change_id: row.id, kind: 'intent', artifactPath: path.relative(rootDir, intentPath),
964
999
  contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
965
- workflow_id: workflow.id,
1000
+ workflow_id: workflow?.id || null,
1001
+ consumer_ref: kernelMode
1002
+ ? { type: 'external', id: 'ultra-plan', relation: 'consumed_by' }
1003
+ : null,
1004
+ writer: kernelMode ? 'ultra.record' : 'change-workflow',
966
1005
  rootDir,
967
1006
  });
968
1007
  if (row.kind === 'incident') {
@@ -973,11 +1012,29 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
973
1012
  artifactPath: path.relative(rootDir, diagnosisPath),
974
1013
  contentHash: crypto.createHash('sha256').update(fs.readFileSync(diagnosisPath)).digest('hex'),
975
1014
  metadata: { required_sections: INCIDENT_DIAGNOSIS_SECTIONS.map(({ key }) => key) },
976
- workflow_id: workflow.id,
1015
+ workflow_id: workflow?.id || null,
1016
+ consumer_ref: kernelMode
1017
+ ? { type: 'external', id: 'ultra-dev', relation: 'consumed_by' }
1018
+ : null,
1019
+ writer: kernelMode ? 'ultra.record' : 'change-workflow',
977
1020
  rootDir,
978
1021
  });
979
1022
  }
980
- workflow = workflows.recordWorkflowStep(db, {
1023
+ if (kernelMode) {
1024
+ return {
1025
+ change: readChange(db, row.id),
1026
+ intent_path: intentPath,
1027
+ context_manifest_path: null,
1028
+ workflow: null,
1029
+ diagnostics: baselineHealth.status === 'pass'
1030
+ ? []
1031
+ : (baselineHealth.blockers || []).map((code) => ({
1032
+ code,
1033
+ severity: 'needs_attention',
1034
+ })),
1035
+ };
1036
+ }
1037
+ workflow = legacyWorkflows().recordWorkflowStep(db, {
981
1038
  id: workflow.id, step_id: 'bind-baseline', status: 'completed',
982
1039
  evidence: [{
983
1040
  kind: 'baseline', ref: baselineHealth.baseline?.id || 'incident-break-glass',
@@ -986,14 +1043,14 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
986
1043
  : 'Bound the approved incident break-glass authority.',
987
1044
  }],
988
1045
  }, { rootDir });
989
- workflow = workflows.recordWorkflowStep(db, {
1046
+ workflow = legacyWorkflows().recordWorkflowStep(db, {
990
1047
  id: workflow.id, step_id: 'classify-change', status: 'completed',
991
1048
  decisions: [{
992
1049
  kind: 'change_kind', value: row.kind,
993
1050
  rationale: row.classification.rationale, risk_flags: row.classification.risk_flags,
994
1051
  }],
995
1052
  }, { rootDir });
996
- workflow = workflows.recordWorkflowStep(db, {
1053
+ workflow = legacyWorkflows().recordWorkflowStep(db, {
997
1054
  id: workflow.id, step_id: 'record-intent', status: 'completed',
998
1055
  evidence: [{ kind: 'intent', ref: path.relative(rootDir, intentPath), summary: row.intent }],
999
1056
  outputs: [{ path: path.relative(rootDir, intentPath), kind: 'change-intent' }],
@@ -1002,7 +1059,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
1002
1059
  research_disposition: row.research_disposition,
1003
1060
  }],
1004
1061
  }, { rootDir });
1005
- workflow = workflows.completeWorkflow(db, { id: workflow.id }, { rootDir });
1062
+ workflow = legacyWorkflows().completeWorkflow(db, { id: workflow.id }, { rootDir });
1006
1063
  return {
1007
1064
  change: readChange(db, row.id), intent_path: intentPath,
1008
1065
  context_manifest_path: null,
@@ -1026,7 +1083,7 @@ function assertTaskCreationAllowed(db, input = {}, { rootDir = process.cwd() } =
1026
1083
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
1027
1084
  if (baselineHealth.status === 'pass') return;
1028
1085
  const gate = change
1029
- ? contextSpine.baselineGateForChange(db, change, baselineHealth)
1086
+ ? legacyContextSpine().baselineGateForChange(db, change, baselineHealth)
1030
1087
  : { blockers: baselineHealth.blockers };
1031
1088
  if (gate.blockers.length === 0) return;
1032
1089
  throw new ChangeWorkflowError(
@@ -1036,7 +1093,10 @@ function assertTaskCreationAllowed(db, input = {}, { rootDir = process.cwd() } =
1036
1093
  );
1037
1094
  }
1038
1095
 
1039
- function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1096
+ function updateChange(db, id, patch = {}, {
1097
+ rootDir = process.cwd(),
1098
+ kernelMode = false,
1099
+ } = {}) {
1040
1100
  const current = readChange(db, id);
1041
1101
  if (!current) throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${id} not found`);
1042
1102
  if (['archived', 'cancelled'].includes(current.status)) {
@@ -1047,6 +1107,13 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1047
1107
  throw new ChangeWorkflowError('VALIDATION_ERROR', `change field ${field} is not patchable`);
1048
1108
  }
1049
1109
  }
1110
+ if (kernelMode && patch.status !== undefined
1111
+ && !['active', 'cancelled'].includes(patch.status)) {
1112
+ throw new ChangeWorkflowError(
1113
+ 'VALIDATION_ERROR',
1114
+ 'kernel Change status may only be active or cancelled; readiness is derived from checkpoints',
1115
+ );
1116
+ }
1050
1117
  const syncIntent = [
1051
1118
  'title', 'intent', 'docs_impact', 'contract', 'classification', 'research_disposition',
1052
1119
  ].some((field) => patch[field] !== undefined);
@@ -1086,14 +1153,17 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1086
1153
  values.push(JSON.stringify(normalizeResearchDisposition(patch.research_disposition, current.kind)));
1087
1154
  }
1088
1155
  if (patch.status !== undefined) {
1089
- const allowed = CHANGE_TRANSITIONS[current.status] || new Set();
1156
+ const allowed = kernelMode
1157
+ ? new Set(['active', 'cancelled'])
1158
+ : (CHANGE_TRANSITIONS[current.status] || new Set());
1090
1159
  if (!allowed.has(patch.status)) {
1091
1160
  throw new ChangeWorkflowError(
1092
1161
  'ILLEGAL_CHANGE_TRANSITION', `cannot transition change ${id} from ${current.status} to ${patch.status}`,
1093
1162
  );
1094
1163
  }
1095
1164
  sets.push('status = ?'); values.push(patch.status);
1096
- } else if (current.status === 'ready' && Object.keys(patch).length > 0) {
1165
+ } else if (['ready', 'blocked'].includes(current.status)
1166
+ && Object.keys(patch).length > 0) {
1097
1167
  sets.push('status = ?'); values.push('active');
1098
1168
  }
1099
1169
  if (sets.length === 0) return current;
@@ -1110,6 +1180,10 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1110
1180
  kind: 'intent',
1111
1181
  artifactPath: path.relative(rootDir, intentPath),
1112
1182
  contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
1183
+ consumer_ref: kernelMode
1184
+ ? { type: 'external', id: 'ultra-plan', relation: 'consumed_by' }
1185
+ : null,
1186
+ writer: kernelMode ? 'ultra.record' : 'change-workflow',
1113
1187
  rootDir,
1114
1188
  });
1115
1189
  intentArtifactId = recorded.artifact.id;
@@ -1118,7 +1192,8 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1118
1192
  const semanticFields = Object.keys(patch)
1119
1193
  .filter((field) => SEMANTIC_AUTHORITY_FIELDS.has(field));
1120
1194
  if (semanticFields.length > 0
1121
- && contextSpine.changeStateDigest(current) !== contextSpine.changeStateDigest(updated)) {
1195
+ && changeAuthority.changeStateDigest(current)
1196
+ !== changeAuthority.changeStateDigest(updated)) {
1122
1197
  invalidated.push(...artifacts.invalidateConsumersFromEndpointInTx(
1123
1198
  db,
1124
1199
  { type: 'change', id },
@@ -1280,9 +1355,63 @@ function generatePacketReconciliation(db, change, packet, rootDir) {
1280
1355
  }
1281
1356
  }
1282
1357
 
1358
+ function generateCallerNoChangeReconciliation(db, change, input, rootDir) {
1359
+ const baseline = baselines.readBaseline(db);
1360
+ const manifest = {
1361
+ $schema: 'ultra-baseline-reconciliation-v1',
1362
+ change_id: change.id,
1363
+ baseline_id: baseline?.id || null,
1364
+ baseline_updates: [],
1365
+ semantic_changes: [],
1366
+ semantic_no_change_reason: String(input.no_baseline_change_reason).trim(),
1367
+ resolved_gap_ids: [],
1368
+ resolved_unknowns: [],
1369
+ verification: [{
1370
+ name: 'explicit archive handoff',
1371
+ command: 'ultra.archive',
1372
+ status: 'pass',
1373
+ evidence: String(input.summary).trim(),
1374
+ }],
1375
+ };
1376
+ const relative = `${change.artifact_root}/baseline-reconciliation.json`;
1377
+ const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
1378
+ const file = safeRelativePath(rootDir, relative);
1379
+ const previous = fs.existsSync(file) ? fs.readFileSync(file) : null;
1380
+ const published = writeManagedFile(rootDir, relative, Buffer.from(serialized));
1381
+ try {
1382
+ const recorded = artifacts.recordArtifact(db, {
1383
+ id: `${change.id}-baseline-reconciliation`,
1384
+ owner_type: 'change',
1385
+ owner_id: change.id,
1386
+ kind: 'baseline_reconciliation',
1387
+ path: relative,
1388
+ content_digest: published.digest,
1389
+ source_refs: [],
1390
+ consumer_refs: [],
1391
+ provenance: { writer: 'ultra.archive', generated: true },
1392
+ metadata: {
1393
+ terminal_role: true,
1394
+ schema: manifest.$schema,
1395
+ caller_declared: true,
1396
+ },
1397
+ }, { rootDir }).artifact;
1398
+ return {
1399
+ manifest,
1400
+ relative,
1401
+ file,
1402
+ digest: recorded.digest,
1403
+ artifact: recorded,
1404
+ };
1405
+ } catch (error) {
1406
+ if (previous === null) fs.rmSync(file, { force: true });
1407
+ else fs.writeFileSync(file, previous);
1408
+ throw error;
1409
+ }
1410
+ }
1411
+
1283
1412
  function baselineConvergenceBlockers(db, change, rootDir) {
1284
1413
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
1285
- return contextSpine.baselineGateForChange(db, change, baselineHealth).blockers;
1414
+ return legacyContextSpine().baselineGateForChange(db, change, baselineHealth).blockers;
1286
1415
  }
1287
1416
 
1288
1417
  function deriveConvergenceEvidence(db, change, tasks, workflowGate, diagnosis, rootDir) {
@@ -1291,7 +1420,7 @@ function deriveConvergenceEvidence(db, change, tasks, workflowGate, diagnosis, r
1291
1420
  WHERE kind = 'dev' AND change_id = ? AND status = 'completed'
1292
1421
  ORDER BY completed_at ASC, rowid ASC`,
1293
1422
  ).all(change.id)
1294
- .filter((row) => workflows.isConsumableWorkflowAuthority({
1423
+ .filter((row) => legacyWorkflows().isConsumableWorkflowAuthority({
1295
1424
  metadata: parseJson(row.metadata_json, 'workflow_runs.metadata_json'),
1296
1425
  }))
1297
1426
  .map((row) => ({
@@ -1302,7 +1431,7 @@ function deriveConvergenceEvidence(db, change, tasks, workflowGate, diagnosis, r
1302
1431
  const deltaPaths = deltaAuthority
1303
1432
  ? [deltaAuthority.path, deltaAuthority.digest]
1304
1433
  : [];
1305
- const appliedLearning = specLearning.listSpecLearning(db, change.id)
1434
+ const appliedLearning = legacySpecLearning().listSpecLearning(db, change.id)
1306
1435
  .filter((candidate) => candidate.status === 'applied')
1307
1436
  .map((candidate) => candidate.target_ref);
1308
1437
  const rows = [
@@ -1364,7 +1493,11 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
1364
1493
  const blockers = new Set();
1365
1494
  let workflowGate = null;
1366
1495
  try {
1367
- workflowGate = workflows.validateDeliveryPrerequisites(db, { change_id: change.id }, rootDir);
1496
+ workflowGate = legacyWorkflows().validateDeliveryPrerequisites(
1497
+ db,
1498
+ { change_id: change.id },
1499
+ rootDir,
1500
+ );
1368
1501
  } catch (error) {
1369
1502
  blockers.add(error.code || 'WORKFLOW_GATE_UNAVAILABLE');
1370
1503
  }
@@ -1373,7 +1506,7 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
1373
1506
  if (tasks.some((task) => !['completed', 'expanded'].includes(task.status))) blockers.add('TASKS_INCOMPLETE');
1374
1507
  if (tasks.some((task) => Boolean(task.stale))) blockers.add('TASK_CONTEXT_STALE');
1375
1508
  for (const task of tasks.filter((item) => item.status !== 'expanded')) {
1376
- const validation = contextSpine.validateContextSnapshot(db, {
1509
+ const validation = legacyContextSpine().validateContextSnapshot(db, {
1377
1510
  change_id: change.id,
1378
1511
  task_id: task.id,
1379
1512
  role: 'implement',
@@ -1394,7 +1527,7 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
1394
1527
  }
1395
1528
  }
1396
1529
 
1397
- for (const candidate of specLearning.listSpecLearning(db, change.id)) {
1530
+ for (const candidate of legacySpecLearning().listSpecLearning(db, change.id)) {
1398
1531
  if (candidate.status === 'proposed') blockers.add(`SPEC_LEARNING_UNRESOLVED:${candidate.id}`);
1399
1532
  if (candidate.status === 'approved') blockers.add(`SPEC_LEARNING_NOT_APPLIED:${candidate.id}`);
1400
1533
  }
@@ -1511,6 +1644,131 @@ function replaceArchiveAuthorityText(
1511
1644
  return result;
1512
1645
  }
1513
1646
 
1647
+ function kernelArchiveContextPath(change, id) {
1648
+ return `${change.artifact_root}/authority/context-envelopes/${id}.json`;
1649
+ }
1650
+
1651
+ function kernelArchiveWorkerPath(change, id) {
1652
+ return `${change.artifact_root}/authority/worker-packets/${id}.json`;
1653
+ }
1654
+
1655
+ function materializeKernelArchiveAuthority(db, change, rootDir) {
1656
+ const taskIds = db.prepare(
1657
+ 'SELECT id FROM tasks WHERE change_id = ? ORDER BY id',
1658
+ ).all(change.id).map((row) => row.id);
1659
+ const contexts = db.prepare(
1660
+ `SELECT * FROM context_envelopes
1661
+ WHERE (scope_type = 'change' AND scope_id = ?)
1662
+ OR (scope_type = 'task' AND scope_id IN (
1663
+ SELECT id FROM tasks WHERE change_id = ?
1664
+ ))
1665
+ ORDER BY id`,
1666
+ ).all(change.id, change.id);
1667
+ for (const row of contexts) {
1668
+ const read = readStableProjectFile(rootDir, row.artifact_path);
1669
+ if (!row.file_digest || read.digest !== row.file_digest) {
1670
+ throw new ChangeWorkflowError(
1671
+ 'CONTEXT_ENVELOPE_FILE_DRIFT',
1672
+ `cannot archive unbound Context Envelope ${row.id}`,
1673
+ {
1674
+ path: row.artifact_path,
1675
+ expected: row.file_digest || null,
1676
+ actual: read.digest,
1677
+ },
1678
+ );
1679
+ }
1680
+ const target = kernelArchiveContextPath(change, row.id);
1681
+ const published = writeManagedFile(rootDir, target, read.bytes);
1682
+ upsertArtifact(db, {
1683
+ change_id: change.id,
1684
+ task_id: row.scope_type === 'task' ? row.scope_id : null,
1685
+ kind: 'archive_context_envelope',
1686
+ artifactPath: target,
1687
+ contentHash: published.digest,
1688
+ consumer_ref: {
1689
+ type: 'external',
1690
+ id: 'ultra.archive',
1691
+ relation: 'archived_by',
1692
+ },
1693
+ writer: 'ultra.archive',
1694
+ rootDir,
1695
+ });
1696
+ }
1697
+ if (taskIds.length === 0) return;
1698
+ const packets = db.prepare(
1699
+ `SELECT * FROM worker_packets
1700
+ WHERE scope_type = 'task' AND status = 'assigned'
1701
+ AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)
1702
+ ORDER BY id`,
1703
+ ).all(change.id);
1704
+ for (const row of packets) {
1705
+ const read = readStableProjectFile(rootDir, row.packet_path);
1706
+ if (!row.file_digest || read.digest !== row.file_digest) {
1707
+ throw new ChangeWorkflowError(
1708
+ 'WORKER_PACKET_FILE_DRIFT',
1709
+ `cannot archive unbound Worker Packet ${row.id}`,
1710
+ {
1711
+ path: row.packet_path,
1712
+ expected: row.file_digest || null,
1713
+ actual: read.digest,
1714
+ },
1715
+ );
1716
+ }
1717
+ const target = kernelArchiveWorkerPath(change, row.id);
1718
+ const published = writeManagedFile(rootDir, target, read.bytes);
1719
+ upsertArtifact(db, {
1720
+ change_id: change.id,
1721
+ task_id: row.scope_id,
1722
+ kind: 'archive_worker_packet',
1723
+ artifactPath: target,
1724
+ contentHash: published.digest,
1725
+ consumer_ref: {
1726
+ type: 'external',
1727
+ id: 'ultra.archive',
1728
+ relation: 'archived_by',
1729
+ },
1730
+ writer: 'ultra.archive',
1731
+ rootDir,
1732
+ });
1733
+ }
1734
+ }
1735
+
1736
+ function normalizedArchiveDocument(bytes, intent) {
1737
+ const text = bytes.toString('utf8');
1738
+ if (!Buffer.from(text, 'utf8').equals(bytes)) return { bytes, semanticDigest: null };
1739
+ let document;
1740
+ try { document = JSON.parse(text); }
1741
+ catch { return { bytes, semanticDigest: null }; }
1742
+ let semanticDigest = null;
1743
+ if (document?.schema_version === '1.0'
1744
+ && document.detail === 'full'
1745
+ && document.envelope
1746
+ && document.digest) {
1747
+ if (document.envelope.change?.id === intent.change_id) {
1748
+ document.envelope.change.status = 'archived';
1749
+ }
1750
+ semanticDigest = canonical.digest(document.envelope);
1751
+ document.digest = semanticDigest;
1752
+ } else if (document?.packet_version && document.packet_digest) {
1753
+ const { packet_digest: _prior, ...value } = document;
1754
+ semanticDigest = canonical.digest(value);
1755
+ document.packet_digest = semanticDigest;
1756
+ } else if (document?.schema_version === '1.0'
1757
+ && document.question
1758
+ && document.selection
1759
+ && document.digest) {
1760
+ const { digest: _prior, ...value } = document;
1761
+ semanticDigest = canonical.digest(value);
1762
+ document.digest = semanticDigest;
1763
+ } else if (document?.change?.id === intent.change_id) {
1764
+ document.change.status = 'archived';
1765
+ }
1766
+ return {
1767
+ bytes: Buffer.from(`${JSON.stringify(document, null, 2)}\n`),
1768
+ semanticDigest,
1769
+ };
1770
+ }
1771
+
1514
1772
  function archiveRebindAuthorityValue(value, source, destination, digestMap) {
1515
1773
  if (typeof value === 'string') {
1516
1774
  return replaceArchiveAuthorityText(
@@ -1546,6 +1804,7 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1546
1804
  );
1547
1805
  }
1548
1806
  const originalByPath = new Map();
1807
+ const semanticByPath = new Map();
1549
1808
  for (const artifact of db.prepare(
1550
1809
  'SELECT path, digest, content_hash FROM artifacts WHERE change_id = ?',
1551
1810
  ).all(intent.change_id)) {
@@ -1567,6 +1826,35 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1567
1826
  originalByPath.set(archivedPath, snapshot.manifest_hash);
1568
1827
  }
1569
1828
  }
1829
+ for (const row of db.prepare(
1830
+ `SELECT id, digest FROM decision_records
1831
+ WHERE scope_type = 'change' AND scope_id = ?`,
1832
+ ).all(intent.change_id)) {
1833
+ const archivedPath = `${intent.destination}/decisions/${row.id}.json`;
1834
+ semanticByPath.set(archivedPath, row.digest);
1835
+ }
1836
+ for (const row of db.prepare(
1837
+ `SELECT id, digest FROM context_envelopes
1838
+ WHERE (scope_type = 'change' AND scope_id = ?)
1839
+ OR (scope_type = 'task' AND scope_id IN (
1840
+ SELECT id FROM tasks WHERE change_id = ?
1841
+ ))`,
1842
+ ).all(intent.change_id, intent.change_id)) {
1843
+ semanticByPath.set(
1844
+ `${intent.destination}/authority/context-envelopes/${row.id}.json`,
1845
+ row.digest,
1846
+ );
1847
+ }
1848
+ for (const row of db.prepare(
1849
+ `SELECT id, packet_digest FROM worker_packets
1850
+ WHERE scope_type = 'task' AND status = 'assigned'
1851
+ AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)`,
1852
+ ).all(intent.change_id)) {
1853
+ semanticByPath.set(
1854
+ `${intent.destination}/authority/worker-packets/${row.id}.json`,
1855
+ row.packet_digest,
1856
+ );
1857
+ }
1570
1858
  const original = new Map();
1571
1859
  const pathRebound = new Map();
1572
1860
  for (const relative of tree.files) {
@@ -1603,7 +1891,10 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1603
1891
  manifest.change.status = 'archived';
1604
1892
  next = `${JSON.stringify(manifest, null, 2)}\n`;
1605
1893
  }
1606
- pathRebound.set(relative, Buffer.from(next));
1894
+ pathRebound.set(
1895
+ relative,
1896
+ normalizedArchiveDocument(Buffer.from(next), intent).bytes,
1897
+ );
1607
1898
  }
1608
1899
 
1609
1900
  let current = new Map(pathRebound);
@@ -1618,6 +1909,14 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1618
1909
  );
1619
1910
  }
1620
1911
  }
1912
+ for (const [relative, beforeDigest] of semanticByPath) {
1913
+ const bytes = current.get(relative);
1914
+ if (!bytes) continue;
1915
+ const normalized = normalizedArchiveDocument(bytes, intent);
1916
+ if (normalized.semanticDigest) {
1917
+ digestMap.set(beforeDigest, normalized.semanticDigest);
1918
+ }
1919
+ }
1621
1920
  const next = new Map();
1622
1921
  let changed = false;
1623
1922
  for (const [relative, baseBytes] of pathRebound) {
@@ -1626,11 +1925,12 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1626
1925
  next.set(relative, baseBytes);
1627
1926
  continue;
1628
1927
  }
1629
- const bytes = Buffer.from(
1928
+ const replaced = Buffer.from(
1630
1929
  replaceArchiveAuthorityText(
1631
1930
  text, intent.source, intent.destination, digestMap,
1632
1931
  ),
1633
1932
  );
1933
+ const bytes = normalizedArchiveDocument(replaced, intent).bytes;
1634
1934
  next.set(relative, bytes);
1635
1935
  if (!bytes.equals(current.get(relative))) changed = true;
1636
1936
  }
@@ -1669,10 +1969,52 @@ function archiveRebindState(db, intent, rootDir) {
1669
1969
  const digestMap = new Map(journal.entries.map(
1670
1970
  (entry) => [entry.before_digest, entry.after_digest],
1671
1971
  ));
1972
+ const semanticEntry = (relativePath) => byPath.get(relativePath);
1973
+ for (const row of db.prepare(
1974
+ `SELECT id, digest FROM decision_records
1975
+ WHERE scope_type = 'change' AND scope_id = ?`,
1976
+ ).all(intent.change_id)) {
1977
+ const entry = semanticEntry(`${intent.destination}/decisions/${row.id}.json`);
1978
+ if (!entry) continue;
1979
+ const document = JSON.parse(Buffer.from(entry.after_base64, 'base64').toString('utf8'));
1980
+ if (document.digest) digestMap.set(row.digest, document.digest);
1981
+ }
1982
+ for (const row of db.prepare(
1983
+ `SELECT id, digest FROM context_envelopes
1984
+ WHERE (scope_type = 'change' AND scope_id = ?)
1985
+ OR (scope_type = 'task' AND scope_id IN (
1986
+ SELECT id FROM tasks WHERE change_id = ?
1987
+ ))`,
1988
+ ).all(intent.change_id, intent.change_id)) {
1989
+ const entry = semanticEntry(
1990
+ `${intent.destination}/authority/context-envelopes/${row.id}.json`,
1991
+ );
1992
+ if (!entry) continue;
1993
+ const document = JSON.parse(Buffer.from(entry.after_base64, 'base64').toString('utf8'));
1994
+ if (document.digest) digestMap.set(row.digest, document.digest);
1995
+ }
1996
+ for (const row of db.prepare(
1997
+ `SELECT id, packet_digest FROM worker_packets
1998
+ WHERE scope_type = 'task' AND status = 'assigned'
1999
+ AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)`,
2000
+ ).all(intent.change_id)) {
2001
+ const entry = semanticEntry(
2002
+ `${intent.destination}/authority/worker-packets/${row.id}.json`,
2003
+ );
2004
+ if (!entry) continue;
2005
+ const document = JSON.parse(Buffer.from(entry.after_base64, 'base64').toString('utf8'));
2006
+ if (document.packet_digest) {
2007
+ digestMap.set(row.packet_digest, document.packet_digest);
2008
+ }
2009
+ }
1672
2010
  return { journal, byPath, digestMap };
1673
2011
  }
1674
2012
 
1675
- function finalizeArchive(db, intent, { rootDir, journalIntent = intent }) {
2013
+ function finalizeArchive(db, intent, {
2014
+ rootDir,
2015
+ journalIntent = intent,
2016
+ kernelMode = false,
2017
+ }) {
1676
2018
  const change = readChange(db, intent.change_id);
1677
2019
  if (!change) throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${intent.change_id} not found`);
1678
2020
  try {
@@ -1690,8 +2032,12 @@ function finalizeArchive(db, intent, { rootDir, journalIntent = intent }) {
1690
2032
  && artifacts.normalizeRelativePath(change.artifact_root) === relative) {
1691
2033
  return { change, archive_path: destination };
1692
2034
  }
1693
- if (change.status !== 'ready') {
1694
- throw new ChangeWorkflowError('CHANGE_NOT_READY', `change ${change.id} must be ready to finish archive`);
2035
+ const acceptedStatuses = kernelMode ? ['active', 'ready'] : ['ready'];
2036
+ if (!acceptedStatuses.includes(change.status)) {
2037
+ throw new ChangeWorkflowError(
2038
+ 'CHANGE_NOT_READY',
2039
+ `change ${change.id} must have complete accepted checkpoints to finish archive`,
2040
+ );
1695
2041
  }
1696
2042
  const rebind = archiveRebindState(db, journalIntent, rootDir);
1697
2043
  const archivedReconciliationPath = rebindRegistryPath(
@@ -1836,6 +2182,210 @@ function finalizeArchive(db, intent, { rootDir, journalIntent = intent }) {
1836
2182
  snapshot.id,
1837
2183
  );
1838
2184
  }
2185
+ for (const decision of db.prepare(
2186
+ `SELECT id FROM decision_records
2187
+ WHERE scope_type = 'change' AND scope_id = ?`,
2188
+ ).all(change.id)) {
2189
+ const archivedPath = `${relative}/decisions/${decision.id}.json`;
2190
+ const rebound = rebind.byPath.get(archivedPath);
2191
+ if (!rebound) {
2192
+ throw new ChangeWorkflowError(
2193
+ 'ARCHIVE_DECISION_MISSING',
2194
+ `archive is missing Decision Record ${decision.id}`,
2195
+ );
2196
+ }
2197
+ const document = JSON.parse(
2198
+ Buffer.from(rebound.after_base64, 'base64').toString('utf8'),
2199
+ );
2200
+ db.prepare(
2201
+ `UPDATE decision_records
2202
+ SET question = ?, recommendation = ?, selection = ?,
2203
+ effects_json = ?, non_goals_json = ?, owner = ?, source = ?,
2204
+ provenance_json = ?, applied_refs_json = ?, digest = ?,
2205
+ artifact_path = ?, updated_at = ?
2206
+ WHERE id = ?`,
2207
+ ).run(
2208
+ document.question,
2209
+ document.recommendation,
2210
+ document.selection,
2211
+ JSON.stringify(document.effects || {}),
2212
+ JSON.stringify(document.non_goals || []),
2213
+ document.owner,
2214
+ document.source,
2215
+ JSON.stringify(document.provenance || {}),
2216
+ JSON.stringify(document.applied_refs || []),
2217
+ document.digest,
2218
+ archivedPath,
2219
+ nowIso(),
2220
+ decision.id,
2221
+ );
2222
+ }
2223
+ for (const envelope of db.prepare(
2224
+ `SELECT id FROM context_envelopes
2225
+ WHERE (scope_type = 'change' AND scope_id = ?)
2226
+ OR (scope_type = 'task' AND scope_id IN (
2227
+ SELECT id FROM tasks WHERE change_id = ?
2228
+ ))`,
2229
+ ).all(change.id, change.id)) {
2230
+ const archivedPath = `${relative}/authority/context-envelopes/${envelope.id}.json`;
2231
+ const rebound = rebind.byPath.get(archivedPath);
2232
+ if (!rebound) {
2233
+ throw new ChangeWorkflowError(
2234
+ 'ARCHIVE_CONTEXT_MISSING',
2235
+ `archive is missing Context Envelope ${envelope.id}`,
2236
+ );
2237
+ }
2238
+ const document = JSON.parse(
2239
+ Buffer.from(rebound.after_base64, 'base64').toString('utf8'),
2240
+ );
2241
+ db.prepare(
2242
+ `UPDATE context_envelopes
2243
+ SET digest = ?, file_digest = ?, payload_json = ?, artifact_path = ?
2244
+ WHERE id = ?`,
2245
+ ).run(
2246
+ document.digest,
2247
+ rebound.after_digest,
2248
+ JSON.stringify(document.envelope),
2249
+ archivedPath,
2250
+ envelope.id,
2251
+ );
2252
+ }
2253
+ for (const packet of db.prepare(
2254
+ `SELECT id FROM worker_packets
2255
+ WHERE scope_type = 'task' AND status = 'assigned'
2256
+ AND scope_id IN (SELECT id FROM tasks WHERE change_id = ?)`,
2257
+ ).all(change.id)) {
2258
+ const archivedPath = `${relative}/authority/worker-packets/${packet.id}.json`;
2259
+ const rebound = rebind.byPath.get(archivedPath);
2260
+ if (!rebound) {
2261
+ throw new ChangeWorkflowError(
2262
+ 'ARCHIVE_WORKER_PACKET_MISSING',
2263
+ `archive is missing Worker Packet ${packet.id}`,
2264
+ );
2265
+ }
2266
+ const document = JSON.parse(
2267
+ Buffer.from(rebound.after_base64, 'base64').toString('utf8'),
2268
+ );
2269
+ const decisionDigest = canonical.digest(
2270
+ (document.accepted_decisions || []).map((decision) => ({
2271
+ id: decision.id,
2272
+ digest: decision.digest,
2273
+ status: 'accepted',
2274
+ })),
2275
+ );
2276
+ db.prepare(
2277
+ `UPDATE worker_packets
2278
+ SET context_digest = ?, task_digest = ?, decision_digest = ?,
2279
+ packet_digest = ?, file_digest = ?, packet_path = ?, output_path = ?
2280
+ WHERE id = ?`,
2281
+ ).run(
2282
+ document.context_envelope?.digest || null,
2283
+ document.task?.digest || null,
2284
+ decisionDigest,
2285
+ document.packet_digest,
2286
+ rebound.after_digest,
2287
+ archivedPath,
2288
+ document.output?.path,
2289
+ packet.id,
2290
+ );
2291
+ }
2292
+ const checkpointRows = db.prepare(
2293
+ `SELECT *
2294
+ FROM stage_checkpoints
2295
+ WHERE (scope_type = 'change' AND scope_id = ?)
2296
+ OR (scope_type = 'task' AND scope_id IN (
2297
+ SELECT id FROM tasks WHERE change_id = ?
2298
+ ))
2299
+ ORDER BY revision`,
2300
+ ).all(change.id, change.id);
2301
+ for (const row of checkpointRows) {
2302
+ const value = {
2303
+ stage: row.stage,
2304
+ scope_type: row.scope_type,
2305
+ scope_id: row.scope_id,
2306
+ revision: row.revision,
2307
+ payload: archiveRebindAuthorityValue(
2308
+ parseJson(row.payload_json || '{}', 'stage_checkpoints.payload_json'),
2309
+ intent.source,
2310
+ relative,
2311
+ rebind.digestMap,
2312
+ ),
2313
+ evidence: archiveRebindAuthorityValue(
2314
+ parseJson(row.evidence_json || '[]', 'stage_checkpoints.evidence_json'),
2315
+ intent.source,
2316
+ relative,
2317
+ rebind.digestMap,
2318
+ ),
2319
+ diagnostics: archiveRebindAuthorityValue(
2320
+ parseJson(row.diagnostics_json || '[]', 'stage_checkpoints.diagnostics_json'),
2321
+ intent.source,
2322
+ relative,
2323
+ rebind.digestMap,
2324
+ ),
2325
+ context_envelope_id: row.context_envelope_id,
2326
+ supersedes_id: row.supersedes_id,
2327
+ };
2328
+ db.prepare(
2329
+ `UPDATE stage_checkpoints
2330
+ SET payload_json = ?, evidence_json = ?, diagnostics_json = ?, digest = ?,
2331
+ updated_at = ?
2332
+ WHERE id = ?`,
2333
+ ).run(
2334
+ JSON.stringify(value.payload),
2335
+ JSON.stringify(value.evidence),
2336
+ JSON.stringify(value.diagnostics),
2337
+ stageCheckpoints.checkpointDigest(value),
2338
+ nowIso(),
2339
+ row.id,
2340
+ );
2341
+ }
2342
+ for (const task of db.prepare(
2343
+ `SELECT id, context_refs_json, docs_impact_json, ownership_json
2344
+ FROM tasks WHERE change_id = ?`,
2345
+ ).all(change.id)) {
2346
+ db.prepare(
2347
+ `UPDATE tasks
2348
+ SET context_refs_json = ?, docs_impact_json = ?, ownership_json = ?,
2349
+ updated_at = ?
2350
+ WHERE id = ?`,
2351
+ ).run(
2352
+ JSON.stringify(archiveRebindAuthorityValue(
2353
+ parseJson(task.context_refs_json || '[]', 'tasks.context_refs_json'),
2354
+ intent.source,
2355
+ relative,
2356
+ rebind.digestMap,
2357
+ )),
2358
+ JSON.stringify(archiveRebindAuthorityValue(
2359
+ parseJson(task.docs_impact_json || '{}', 'tasks.docs_impact_json'),
2360
+ intent.source,
2361
+ relative,
2362
+ rebind.digestMap,
2363
+ )),
2364
+ JSON.stringify(archiveRebindAuthorityValue(
2365
+ parseJson(task.ownership_json || '{}', 'tasks.ownership_json'),
2366
+ intent.source,
2367
+ relative,
2368
+ rebind.digestMap,
2369
+ )),
2370
+ nowIso(),
2371
+ task.id,
2372
+ );
2373
+ }
2374
+ for (const event of db.prepare(
2375
+ `SELECT id, payload_json FROM events
2376
+ WHERE change_id = ?
2377
+ OR task_id IN (SELECT id FROM tasks WHERE change_id = ?)`,
2378
+ ).all(change.id, change.id)) {
2379
+ db.prepare('UPDATE events SET payload_json = ? WHERE id = ?').run(
2380
+ JSON.stringify(archiveRebindAuthorityValue(
2381
+ parseJson(event.payload_json || '{}', 'events.payload_json'),
2382
+ intent.source,
2383
+ relative,
2384
+ rebind.digestMap,
2385
+ )),
2386
+ event.id,
2387
+ );
2388
+ }
1839
2389
  for (const run of db.prepare(
1840
2390
  `SELECT id, metadata_json, blockers_json, summary_json, approval_json
1841
2391
  FROM workflow_runs WHERE change_id = ?`,
@@ -1964,13 +2514,19 @@ function finalizeArchive(db, intent, { rootDir, journalIntent = intent }) {
1964
2514
  };
1965
2515
  }
1966
2516
 
1967
- function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
2517
+ function archiveChange(db, input, {
2518
+ rootDir = process.cwd(),
2519
+ kernelMode = false,
2520
+ } = {}) {
1968
2521
  const change = readChange(db, input.id);
1969
2522
  if (!change) throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${input.id} not found`);
1970
2523
  if (!input.summary || String(input.summary).trim().length < 3) {
1971
2524
  throw new ChangeWorkflowError('VALIDATION_ERROR', 'archive summary required');
1972
2525
  }
1973
- if (!['ready', 'archived'].includes(change.status)) {
2526
+ const allowedArchiveStatuses = kernelMode
2527
+ ? ['active', 'archived']
2528
+ : ['ready', 'archived'];
2529
+ if (!allowedArchiveStatuses.includes(change.status)) {
1974
2530
  throw new ChangeWorkflowError('CHANGE_NOT_READY', `change ${input.id} must converge before archive`);
1975
2531
  }
1976
2532
  if (change.status === 'archived') {
@@ -1987,7 +2543,13 @@ function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
1987
2543
 
1988
2544
  let packet = null;
1989
2545
  if (changePacket.readDeltaAuthority(db, change.id)) {
1990
- workflows.validateDeliveryPrerequisites(db, { change_id: change.id }, rootDir);
2546
+ if (!kernelMode) {
2547
+ legacyWorkflows().validateDeliveryPrerequisites(
2548
+ db,
2549
+ { change_id: change.id },
2550
+ rootDir,
2551
+ );
2552
+ }
1991
2553
  packet = changePacket.deliveryEntries(db, change, { rootDir });
1992
2554
  }
1993
2555
  const requestedUpdates = Array.isArray(input.baseline_updates)
@@ -2017,7 +2579,17 @@ function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
2017
2579
 
2018
2580
  const reconciliation = packet
2019
2581
  ? generatePacketReconciliation(db, change, packet, rootDir)
2020
- : readReconciliationManifest(db, change, input, rootDir);
2582
+ : (
2583
+ kernelMode && updates.length === 0 && !String(input.reconciliation_path || '').trim()
2584
+ ? generateCallerNoChangeReconciliation(db, change, {
2585
+ summary: input.summary,
2586
+ no_baseline_change_reason: noChangeReason,
2587
+ }, rootDir)
2588
+ : readReconciliationManifest(db, change, input, rootDir)
2589
+ );
2590
+ if (kernelMode) {
2591
+ materializeKernelArchiveAuthority(db, change, rootDir);
2592
+ }
2021
2593
  let delivery = null;
2022
2594
  let prepared = null;
2023
2595
  try {
@@ -2070,6 +2642,7 @@ function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
2070
2642
  result = finalizeArchive(db, authoritativeIntent, {
2071
2643
  rootDir,
2072
2644
  journalIntent: prepared.intent,
2645
+ kernelMode,
2073
2646
  });
2074
2647
  } catch (error) {
2075
2648
  try { archiveJournal.rollbackArchiveIntent(rootDir, prepared.intent); }
@@ -2126,7 +2699,7 @@ function recoverInterruptedArchives(db, { rootDir = process.cwd() } = {}) {
2126
2699
  archiveJournal.completeArchiveIntent(rootDir, record.intent);
2127
2700
  result.cleaned += 1;
2128
2701
  result.items.push({ change_id: change.id, status: 'cleaned' });
2129
- } else if (change?.status === 'ready' && record.location === 'destination') {
2702
+ } else if (['active', 'ready'].includes(change?.status) && record.location === 'destination') {
2130
2703
  const reconciliation = readReconciliationManifest(db, change, {
2131
2704
  reconciliation_path: record.intent.reconciliation_path,
2132
2705
  baseline_updates: record.intent.baseline_updates,
@@ -2139,6 +2712,10 @@ function recoverInterruptedArchives(db, { rootDir = process.cwd() } = {}) {
2139
2712
  finalizeArchive(db, authoritativeIntent, {
2140
2713
  rootDir,
2141
2714
  journalIntent: record.intent,
2715
+ // Recovery is a mechanical continuation of an already prepared
2716
+ // archive intent. It must never re-enter the retired semantic
2717
+ // workflow supervisor, including for a v0.23 `ready` Change.
2718
+ kernelMode: true,
2142
2719
  });
2143
2720
  archiveJournal.completeArchiveIntent(rootDir, record.intent);
2144
2721
  result.resumed += 1;
@@ -2185,10 +2762,18 @@ module.exports = {
2185
2762
  recordDelta,
2186
2763
  recordDocumentationReconciliation,
2187
2764
  compileContext,
2188
- readBreadcrumb: contextSpine.readBreadcrumb,
2189
- proposeSpecLearning: specLearning.proposeSpecLearning,
2190
- resolveSpecLearning: specLearning.resolveSpecLearning,
2191
- listSpecLearning: specLearning.listSpecLearning,
2765
+ readBreadcrumb(...args) {
2766
+ return legacyContextSpine().readBreadcrumb(...args);
2767
+ },
2768
+ proposeSpecLearning(...args) {
2769
+ return legacySpecLearning().proposeSpecLearning(...args);
2770
+ },
2771
+ resolveSpecLearning(...args) {
2772
+ return legacySpecLearning().resolveSpecLearning(...args);
2773
+ },
2774
+ listSpecLearning(...args) {
2775
+ return legacySpecLearning().listSpecLearning(...args);
2776
+ },
2192
2777
  convergeChange,
2193
2778
  archiveChange,
2194
2779
  normalizeDocsImpact,