ultra-builder-pro-cli 0.23.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 (91) hide show
  1. package/.ultra-template/docs/research/README.md +4 -4
  2. package/CHANGELOG.md +45 -0
  3. package/README.md +21 -18
  4. package/adapters/_shared/codex-assets.cjs +121 -65
  5. package/adapters/_shared/interaction-contract.cjs +7 -1
  6. package/adapters/_shared/provenance.cjs +7 -2
  7. package/adapters/_shared/runtime-assets.cjs +3 -2
  8. package/adapters/_shared/runtime-native.cjs +132 -0
  9. package/adapters/claude.js +60 -30
  10. package/adapters/codex.js +178 -81
  11. package/adapters/grok.js +372 -0
  12. package/adapters/kimi.js +7 -3
  13. package/adapters/opencode.js +210 -32
  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 -229
  27. package/docs/ARCHITECTURE.md +203 -494
  28. package/docs/ARTIFACT-AUTHORITY.md +26 -22
  29. package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
  30. package/docs/ROADMAP.md +85 -110
  31. package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
  32. package/docs/SKILL-AUTHORING.md +3 -3
  33. package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
  34. package/docs/WORKFLOW-LIFECYCLE.md +174 -448
  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/baseline-workflow.cjs +36 -38
  47. package/mcp-server/lib/canonical-json.cjs +19 -0
  48. package/mcp-server/lib/change-workflow.cjs +596 -44
  49. package/mcp-server/lib/context-envelope.cjs +508 -0
  50. package/mcp-server/lib/decision-records.cjs +337 -0
  51. package/mcp-server/lib/doctor.cjs +91 -33
  52. package/mcp-server/lib/init-project.cjs +78 -39
  53. package/mcp-server/lib/managed-file-write.cjs +116 -0
  54. package/mcp-server/lib/plan-checkpoint.cjs +177 -0
  55. package/mcp-server/lib/project-context.cjs +178 -0
  56. package/mcp-server/lib/stage-checkpoints.cjs +309 -0
  57. package/mcp-server/lib/state-db.cjs +155 -24
  58. package/mcp-server/lib/task-ledger.cjs +522 -13
  59. package/mcp-server/lib/ultra-facade.cjs +630 -442
  60. package/mcp-server/lib/worker-packet.cjs +358 -0
  61. package/mcp-server/server.cjs +200 -1103
  62. package/orchestrator/daemon.cjs +27 -9
  63. package/orchestrator/parallel-orchestrator.cjs +111 -76
  64. package/orchestrator/session-runner.cjs +135 -34
  65. package/package.json +10 -3
  66. package/skills/ultra-change/SKILL.md +9 -8
  67. package/skills/ultra-deliver/SKILL.md +3 -3
  68. package/skills/ultra-dev/SKILL.md +18 -11
  69. package/skills/ultra-doctor/SKILL.md +2 -2
  70. package/skills/ultra-init/SKILL.md +6 -5
  71. package/skills/ultra-init/references/brownfield-adoption.md +11 -8
  72. package/skills/ultra-plan/SKILL.md +10 -8
  73. package/skills/ultra-research/SKILL.md +12 -7
  74. package/skills/ultra-review/SKILL.md +9 -6
  75. package/skills/ultra-review/references/unified-schema.md +2 -0
  76. package/skills/ultra-review/scripts/review_wait.py +8 -0
  77. package/skills/ultra-status/SKILL.md +2 -2
  78. package/skills/ultra-test/SKILL.md +4 -2
  79. package/skills/ultra-think/SKILL.md +7 -7
  80. package/skills/ultra-think/references/interaction-boundary.md +56 -0
  81. package/spec/cli-protocol.md +80 -199
  82. package/spec/mcp-tools.yaml +37 -2804
  83. package/spec/schemas/state-db.sql +107 -4
  84. package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
  85. package/ultra-tools/commands/status.cjs +369 -243
  86. package/mcp-server/lib/context-spine.cjs +0 -1208
  87. package/mcp-server/lib/decision-dialogue.cjs +0 -1128
  88. package/mcp-server/lib/project-breadcrumb.cjs +0 -173
  89. package/mcp-server/lib/spec-learning.cjs +0 -284
  90. package/mcp-server/lib/workflow-state.cjs +0 -3129
  91. package/skills/ultra-think/references/decision-dialogue.md +0 -155
@@ -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',
@@ -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,7 +726,10 @@ 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;
@@ -830,7 +861,10 @@ function compileContext(db, input, { rootDir = process.cwd() } = {}) {
830
861
  };
831
862
  }
832
863
 
833
- function createChange(db, input, { rootDir = process.cwd() } = {}) {
864
+ function createChange(db, input, {
865
+ rootDir = process.cwd(),
866
+ kernelMode = false,
867
+ } = {}) {
834
868
  const title = typeof input?.title === 'string' ? input.title.trim() : '';
835
869
  const intent = typeof input?.intent === 'string' ? input.intent.trim() : '';
836
870
  if (!input || !CHANGE_ID.test(input.id || '') || title.length < 3 || intent.length < 3) {
@@ -850,7 +884,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
850
884
  if (readChange(db, input.id)) throw new ChangeWorkflowError('DUPLICATE_CHANGE_ID', `change ${input.id} exists`);
851
885
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
852
886
  let baselineBypass = null;
853
- if (baselineHealth.status !== 'pass') {
887
+ if (baselineHealth.status !== 'pass' && !kernelMode) {
854
888
  if (input.kind !== 'incident') {
855
889
  throw new ChangeWorkflowError(
856
890
  'BASELINE_NOT_READY',
@@ -864,7 +898,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
864
898
  'VALIDATION_ERROR', 'baseline_bypass is not valid while the project baseline is healthy',
865
899
  );
866
900
  }
867
- const alignmentGate = decisions.decisionGate(
901
+ const alignmentGate = kernelMode ? { ready: true, blockers: [] } : legacyDecisions().decisionGate(
868
902
  db, { baseline_id: baselineHealth.baseline?.id || null }, { rootDir },
869
903
  );
870
904
  if (!alignmentGate.ready) {
@@ -882,7 +916,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
882
916
  let alignmentThread = null;
883
917
  if (input.alignment_thread_id !== undefined) {
884
918
  try {
885
- alignmentThread = decisions.assertConfirmedDecisionCheckpoint(
919
+ alignmentThread = legacyDecisions().assertConfirmedDecisionCheckpoint(
886
920
  db, input.alignment_thread_id, { rootDir, requireArtifact: true },
887
921
  );
888
922
  } catch (error) {
@@ -954,7 +988,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
954
988
  alignment_thread_id: row.alignment_thread_id,
955
989
  },
956
990
  });
957
- let workflow = workflows.startWorkflow(db, {
991
+ let workflow = kernelMode ? null : legacyWorkflows().startWorkflow(db, {
958
992
  kind: 'change', baseline_id: baselineHealth.baseline?.id || null,
959
993
  change_id: row.id, subject: `Establish executable readiness for ${row.title}.`,
960
994
  metadata: { change_kind: row.kind, base_commit: row.base_commit },
@@ -962,7 +996,11 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
962
996
  upsertArtifact(db, {
963
997
  change_id: row.id, kind: 'intent', artifactPath: path.relative(rootDir, intentPath),
964
998
  contentHash: crypto.createHash('sha256').update(fs.readFileSync(intentPath)).digest('hex'),
965
- 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',
966
1004
  rootDir,
967
1005
  });
968
1006
  if (row.kind === 'incident') {
@@ -973,11 +1011,29 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
973
1011
  artifactPath: path.relative(rootDir, diagnosisPath),
974
1012
  contentHash: crypto.createHash('sha256').update(fs.readFileSync(diagnosisPath)).digest('hex'),
975
1013
  metadata: { required_sections: INCIDENT_DIAGNOSIS_SECTIONS.map(({ key }) => key) },
976
- 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',
977
1019
  rootDir,
978
1020
  });
979
1021
  }
980
- 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, {
981
1037
  id: workflow.id, step_id: 'bind-baseline', status: 'completed',
982
1038
  evidence: [{
983
1039
  kind: 'baseline', ref: baselineHealth.baseline?.id || 'incident-break-glass',
@@ -986,14 +1042,14 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
986
1042
  : 'Bound the approved incident break-glass authority.',
987
1043
  }],
988
1044
  }, { rootDir });
989
- workflow = workflows.recordWorkflowStep(db, {
1045
+ workflow = legacyWorkflows().recordWorkflowStep(db, {
990
1046
  id: workflow.id, step_id: 'classify-change', status: 'completed',
991
1047
  decisions: [{
992
1048
  kind: 'change_kind', value: row.kind,
993
1049
  rationale: row.classification.rationale, risk_flags: row.classification.risk_flags,
994
1050
  }],
995
1051
  }, { rootDir });
996
- workflow = workflows.recordWorkflowStep(db, {
1052
+ workflow = legacyWorkflows().recordWorkflowStep(db, {
997
1053
  id: workflow.id, step_id: 'record-intent', status: 'completed',
998
1054
  evidence: [{ kind: 'intent', ref: path.relative(rootDir, intentPath), summary: row.intent }],
999
1055
  outputs: [{ path: path.relative(rootDir, intentPath), kind: 'change-intent' }],
@@ -1002,7 +1058,7 @@ function createChange(db, input, { rootDir = process.cwd() } = {}) {
1002
1058
  research_disposition: row.research_disposition,
1003
1059
  }],
1004
1060
  }, { rootDir });
1005
- workflow = workflows.completeWorkflow(db, { id: workflow.id }, { rootDir });
1061
+ workflow = legacyWorkflows().completeWorkflow(db, { id: workflow.id }, { rootDir });
1006
1062
  return {
1007
1063
  change: readChange(db, row.id), intent_path: intentPath,
1008
1064
  context_manifest_path: null,
@@ -1026,7 +1082,7 @@ function assertTaskCreationAllowed(db, input = {}, { rootDir = process.cwd() } =
1026
1082
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
1027
1083
  if (baselineHealth.status === 'pass') return;
1028
1084
  const gate = change
1029
- ? contextSpine.baselineGateForChange(db, change, baselineHealth)
1085
+ ? legacyContextSpine().baselineGateForChange(db, change, baselineHealth)
1030
1086
  : { blockers: baselineHealth.blockers };
1031
1087
  if (gate.blockers.length === 0) return;
1032
1088
  throw new ChangeWorkflowError(
@@ -1036,7 +1092,10 @@ function assertTaskCreationAllowed(db, input = {}, { rootDir = process.cwd() } =
1036
1092
  );
1037
1093
  }
1038
1094
 
1039
- function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1095
+ function updateChange(db, id, patch = {}, {
1096
+ rootDir = process.cwd(),
1097
+ kernelMode = false,
1098
+ } = {}) {
1040
1099
  const current = readChange(db, id);
1041
1100
  if (!current) throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${id} not found`);
1042
1101
  if (['archived', 'cancelled'].includes(current.status)) {
@@ -1047,6 +1106,13 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1047
1106
  throw new ChangeWorkflowError('VALIDATION_ERROR', `change field ${field} is not patchable`);
1048
1107
  }
1049
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
+ }
1050
1116
  const syncIntent = [
1051
1117
  'title', 'intent', 'docs_impact', 'contract', 'classification', 'research_disposition',
1052
1118
  ].some((field) => patch[field] !== undefined);
@@ -1086,14 +1152,17 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1086
1152
  values.push(JSON.stringify(normalizeResearchDisposition(patch.research_disposition, current.kind)));
1087
1153
  }
1088
1154
  if (patch.status !== undefined) {
1089
- const allowed = CHANGE_TRANSITIONS[current.status] || new Set();
1155
+ const allowed = kernelMode
1156
+ ? new Set(['active', 'cancelled'])
1157
+ : (CHANGE_TRANSITIONS[current.status] || new Set());
1090
1158
  if (!allowed.has(patch.status)) {
1091
1159
  throw new ChangeWorkflowError(
1092
1160
  'ILLEGAL_CHANGE_TRANSITION', `cannot transition change ${id} from ${current.status} to ${patch.status}`,
1093
1161
  );
1094
1162
  }
1095
1163
  sets.push('status = ?'); values.push(patch.status);
1096
- } else if (current.status === 'ready' && Object.keys(patch).length > 0) {
1164
+ } else if (['ready', 'blocked'].includes(current.status)
1165
+ && Object.keys(patch).length > 0) {
1097
1166
  sets.push('status = ?'); values.push('active');
1098
1167
  }
1099
1168
  if (sets.length === 0) return current;
@@ -1110,6 +1179,10 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1110
1179
  kind: 'intent',
1111
1180
  artifactPath: path.relative(rootDir, intentPath),
1112
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',
1113
1186
  rootDir,
1114
1187
  });
1115
1188
  intentArtifactId = recorded.artifact.id;
@@ -1118,7 +1191,8 @@ function updateChange(db, id, patch = {}, { rootDir = process.cwd() } = {}) {
1118
1191
  const semanticFields = Object.keys(patch)
1119
1192
  .filter((field) => SEMANTIC_AUTHORITY_FIELDS.has(field));
1120
1193
  if (semanticFields.length > 0
1121
- && contextSpine.changeStateDigest(current) !== contextSpine.changeStateDigest(updated)) {
1194
+ && legacyContextSpine().changeStateDigest(current)
1195
+ !== legacyContextSpine().changeStateDigest(updated)) {
1122
1196
  invalidated.push(...artifacts.invalidateConsumersFromEndpointInTx(
1123
1197
  db,
1124
1198
  { type: 'change', id },
@@ -1282,7 +1356,7 @@ function generatePacketReconciliation(db, change, packet, rootDir) {
1282
1356
 
1283
1357
  function baselineConvergenceBlockers(db, change, rootDir) {
1284
1358
  const baselineHealth = baselines.inspectBaseline(db, { rootDir });
1285
- return contextSpine.baselineGateForChange(db, change, baselineHealth).blockers;
1359
+ return legacyContextSpine().baselineGateForChange(db, change, baselineHealth).blockers;
1286
1360
  }
1287
1361
 
1288
1362
  function deriveConvergenceEvidence(db, change, tasks, workflowGate, diagnosis, rootDir) {
@@ -1291,7 +1365,7 @@ function deriveConvergenceEvidence(db, change, tasks, workflowGate, diagnosis, r
1291
1365
  WHERE kind = 'dev' AND change_id = ? AND status = 'completed'
1292
1366
  ORDER BY completed_at ASC, rowid ASC`,
1293
1367
  ).all(change.id)
1294
- .filter((row) => workflows.isConsumableWorkflowAuthority({
1368
+ .filter((row) => legacyWorkflows().isConsumableWorkflowAuthority({
1295
1369
  metadata: parseJson(row.metadata_json, 'workflow_runs.metadata_json'),
1296
1370
  }))
1297
1371
  .map((row) => ({
@@ -1302,7 +1376,7 @@ function deriveConvergenceEvidence(db, change, tasks, workflowGate, diagnosis, r
1302
1376
  const deltaPaths = deltaAuthority
1303
1377
  ? [deltaAuthority.path, deltaAuthority.digest]
1304
1378
  : [];
1305
- const appliedLearning = specLearning.listSpecLearning(db, change.id)
1379
+ const appliedLearning = legacySpecLearning().listSpecLearning(db, change.id)
1306
1380
  .filter((candidate) => candidate.status === 'applied')
1307
1381
  .map((candidate) => candidate.target_ref);
1308
1382
  const rows = [
@@ -1364,7 +1438,11 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
1364
1438
  const blockers = new Set();
1365
1439
  let workflowGate = null;
1366
1440
  try {
1367
- workflowGate = workflows.validateDeliveryPrerequisites(db, { change_id: change.id }, rootDir);
1441
+ workflowGate = legacyWorkflows().validateDeliveryPrerequisites(
1442
+ db,
1443
+ { change_id: change.id },
1444
+ rootDir,
1445
+ );
1368
1446
  } catch (error) {
1369
1447
  blockers.add(error.code || 'WORKFLOW_GATE_UNAVAILABLE');
1370
1448
  }
@@ -1373,7 +1451,7 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
1373
1451
  if (tasks.some((task) => !['completed', 'expanded'].includes(task.status))) blockers.add('TASKS_INCOMPLETE');
1374
1452
  if (tasks.some((task) => Boolean(task.stale))) blockers.add('TASK_CONTEXT_STALE');
1375
1453
  for (const task of tasks.filter((item) => item.status !== 'expanded')) {
1376
- const validation = contextSpine.validateContextSnapshot(db, {
1454
+ const validation = legacyContextSpine().validateContextSnapshot(db, {
1377
1455
  change_id: change.id,
1378
1456
  task_id: task.id,
1379
1457
  role: 'implement',
@@ -1394,7 +1472,7 @@ function convergeChange(db, input, { rootDir = process.cwd() } = {}) {
1394
1472
  }
1395
1473
  }
1396
1474
 
1397
- for (const candidate of specLearning.listSpecLearning(db, change.id)) {
1475
+ for (const candidate of legacySpecLearning().listSpecLearning(db, change.id)) {
1398
1476
  if (candidate.status === 'proposed') blockers.add(`SPEC_LEARNING_UNRESOLVED:${candidate.id}`);
1399
1477
  if (candidate.status === 'approved') blockers.add(`SPEC_LEARNING_NOT_APPLIED:${candidate.id}`);
1400
1478
  }
@@ -1511,6 +1589,131 @@ function replaceArchiveAuthorityText(
1511
1589
  return result;
1512
1590
  }
1513
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
+
1514
1717
  function archiveRebindAuthorityValue(value, source, destination, digestMap) {
1515
1718
  if (typeof value === 'string') {
1516
1719
  return replaceArchiveAuthorityText(
@@ -1546,6 +1749,7 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1546
1749
  );
1547
1750
  }
1548
1751
  const originalByPath = new Map();
1752
+ const semanticByPath = new Map();
1549
1753
  for (const artifact of db.prepare(
1550
1754
  'SELECT path, digest, content_hash FROM artifacts WHERE change_id = ?',
1551
1755
  ).all(intent.change_id)) {
@@ -1567,6 +1771,35 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1567
1771
  originalByPath.set(archivedPath, snapshot.manifest_hash);
1568
1772
  }
1569
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
+ }
1570
1803
  const original = new Map();
1571
1804
  const pathRebound = new Map();
1572
1805
  for (const relative of tree.files) {
@@ -1603,7 +1836,10 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1603
1836
  manifest.change.status = 'archived';
1604
1837
  next = `${JSON.stringify(manifest, null, 2)}\n`;
1605
1838
  }
1606
- pathRebound.set(relative, Buffer.from(next));
1839
+ pathRebound.set(
1840
+ relative,
1841
+ normalizedArchiveDocument(Buffer.from(next), intent).bytes,
1842
+ );
1607
1843
  }
1608
1844
 
1609
1845
  let current = new Map(pathRebound);
@@ -1618,6 +1854,14 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1618
1854
  );
1619
1855
  }
1620
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
+ }
1621
1865
  const next = new Map();
1622
1866
  let changed = false;
1623
1867
  for (const [relative, baseBytes] of pathRebound) {
@@ -1626,11 +1870,12 @@ function buildArchiveRebindEntries(db, intent, rootDir) {
1626
1870
  next.set(relative, baseBytes);
1627
1871
  continue;
1628
1872
  }
1629
- const bytes = Buffer.from(
1873
+ const replaced = Buffer.from(
1630
1874
  replaceArchiveAuthorityText(
1631
1875
  text, intent.source, intent.destination, digestMap,
1632
1876
  ),
1633
1877
  );
1878
+ const bytes = normalizedArchiveDocument(replaced, intent).bytes;
1634
1879
  next.set(relative, bytes);
1635
1880
  if (!bytes.equals(current.get(relative))) changed = true;
1636
1881
  }
@@ -1669,10 +1914,52 @@ function archiveRebindState(db, intent, rootDir) {
1669
1914
  const digestMap = new Map(journal.entries.map(
1670
1915
  (entry) => [entry.before_digest, entry.after_digest],
1671
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
+ }
1672
1955
  return { journal, byPath, digestMap };
1673
1956
  }
1674
1957
 
1675
- function finalizeArchive(db, intent, { rootDir, journalIntent = intent }) {
1958
+ function finalizeArchive(db, intent, {
1959
+ rootDir,
1960
+ journalIntent = intent,
1961
+ kernelMode = false,
1962
+ }) {
1676
1963
  const change = readChange(db, intent.change_id);
1677
1964
  if (!change) throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${intent.change_id} not found`);
1678
1965
  try {
@@ -1690,8 +1977,12 @@ function finalizeArchive(db, intent, { rootDir, journalIntent = intent }) {
1690
1977
  && artifacts.normalizeRelativePath(change.artifact_root) === relative) {
1691
1978
  return { change, archive_path: destination };
1692
1979
  }
1693
- if (change.status !== 'ready') {
1694
- 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
+ );
1695
1986
  }
1696
1987
  const rebind = archiveRebindState(db, journalIntent, rootDir);
1697
1988
  const archivedReconciliationPath = rebindRegistryPath(
@@ -1836,6 +2127,210 @@ function finalizeArchive(db, intent, { rootDir, journalIntent = intent }) {
1836
2127
  snapshot.id,
1837
2128
  );
1838
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
+ }
1839
2334
  for (const run of db.prepare(
1840
2335
  `SELECT id, metadata_json, blockers_json, summary_json, approval_json
1841
2336
  FROM workflow_runs WHERE change_id = ?`,
@@ -1964,13 +2459,19 @@ function finalizeArchive(db, intent, { rootDir, journalIntent = intent }) {
1964
2459
  };
1965
2460
  }
1966
2461
 
1967
- function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
2462
+ function archiveChange(db, input, {
2463
+ rootDir = process.cwd(),
2464
+ kernelMode = false,
2465
+ } = {}) {
1968
2466
  const change = readChange(db, input.id);
1969
2467
  if (!change) throw new ChangeWorkflowError('CHANGE_NOT_FOUND', `change ${input.id} not found`);
1970
2468
  if (!input.summary || String(input.summary).trim().length < 3) {
1971
2469
  throw new ChangeWorkflowError('VALIDATION_ERROR', 'archive summary required');
1972
2470
  }
1973
- if (!['ready', 'archived'].includes(change.status)) {
2471
+ const allowedArchiveStatuses = kernelMode
2472
+ ? ['active', 'archived']
2473
+ : ['ready', 'archived'];
2474
+ if (!allowedArchiveStatuses.includes(change.status)) {
1974
2475
  throw new ChangeWorkflowError('CHANGE_NOT_READY', `change ${input.id} must converge before archive`);
1975
2476
  }
1976
2477
  if (change.status === 'archived') {
@@ -1986,8 +2487,43 @@ function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
1986
2487
  }
1987
2488
 
1988
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
+ }
1989
2519
  if (changePacket.readDeltaAuthority(db, change.id)) {
1990
- 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
+ }
1991
2527
  packet = changePacket.deliveryEntries(db, change, { rootDir });
1992
2528
  }
1993
2529
  const requestedUpdates = Array.isArray(input.baseline_updates)
@@ -2018,6 +2554,9 @@ function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
2018
2554
  const reconciliation = packet
2019
2555
  ? generatePacketReconciliation(db, change, packet, rootDir)
2020
2556
  : readReconciliationManifest(db, change, input, rootDir);
2557
+ if (kernelMode) {
2558
+ materializeKernelArchiveAuthority(db, change, rootDir);
2559
+ }
2021
2560
  let delivery = null;
2022
2561
  let prepared = null;
2023
2562
  try {
@@ -2070,6 +2609,7 @@ function archiveChange(db, input, { rootDir = process.cwd() } = {}) {
2070
2609
  result = finalizeArchive(db, authoritativeIntent, {
2071
2610
  rootDir,
2072
2611
  journalIntent: prepared.intent,
2612
+ kernelMode,
2073
2613
  });
2074
2614
  } catch (error) {
2075
2615
  try { archiveJournal.rollbackArchiveIntent(rootDir, prepared.intent); }
@@ -2126,7 +2666,7 @@ function recoverInterruptedArchives(db, { rootDir = process.cwd() } = {}) {
2126
2666
  archiveJournal.completeArchiveIntent(rootDir, record.intent);
2127
2667
  result.cleaned += 1;
2128
2668
  result.items.push({ change_id: change.id, status: 'cleaned' });
2129
- } else if (change?.status === 'ready' && record.location === 'destination') {
2669
+ } else if (['active', 'ready'].includes(change?.status) && record.location === 'destination') {
2130
2670
  const reconciliation = readReconciliationManifest(db, change, {
2131
2671
  reconciliation_path: record.intent.reconciliation_path,
2132
2672
  baseline_updates: record.intent.baseline_updates,
@@ -2139,6 +2679,10 @@ function recoverInterruptedArchives(db, { rootDir = process.cwd() } = {}) {
2139
2679
  finalizeArchive(db, authoritativeIntent, {
2140
2680
  rootDir,
2141
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,
2142
2686
  });
2143
2687
  archiveJournal.completeArchiveIntent(rootDir, record.intent);
2144
2688
  result.resumed += 1;
@@ -2185,10 +2729,18 @@ module.exports = {
2185
2729
  recordDelta,
2186
2730
  recordDocumentationReconciliation,
2187
2731
  compileContext,
2188
- readBreadcrumb: contextSpine.readBreadcrumb,
2189
- proposeSpecLearning: specLearning.proposeSpecLearning,
2190
- resolveSpecLearning: specLearning.resolveSpecLearning,
2191
- 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
+ },
2192
2744
  convergeChange,
2193
2745
  archiveChange,
2194
2746
  normalizeDocsImpact,