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.
- package/.ultra-template/docs/research/README.md +4 -4
- package/CHANGELOG.md +45 -0
- package/README.md +21 -18
- package/adapters/_shared/codex-assets.cjs +121 -65
- package/adapters/_shared/interaction-contract.cjs +7 -1
- package/adapters/_shared/provenance.cjs +7 -2
- package/adapters/_shared/runtime-assets.cjs +3 -2
- package/adapters/_shared/runtime-native.cjs +132 -0
- package/adapters/claude.js +60 -30
- package/adapters/codex.js +178 -81
- package/adapters/grok.js +372 -0
- package/adapters/kimi.js +7 -3
- package/adapters/opencode.js +210 -32
- package/agents/code-reviewer.md +4 -2
- package/agents/debugger.md +4 -1
- package/agents/review-code.md +7 -3
- package/agents/review-comments.md +7 -4
- package/agents/review-coordinator.md +11 -7
- package/agents/review-design.md +8 -4
- package/agents/review-errors.md +8 -4
- package/agents/review-spec.md +8 -5
- package/agents/review-tests.md +7 -4
- package/agents/tdd-runner.md +5 -2
- package/bin/install.js +9 -4
- package/bin/orchestrator.js +4 -13
- package/docs/AGENT-CONTEXT.md +141 -229
- package/docs/ARCHITECTURE.md +203 -494
- package/docs/ARTIFACT-AUTHORITY.md +26 -22
- package/docs/PLUGIN-ISOLATION-CONTRACT.md +12 -9
- package/docs/ROADMAP.md +85 -110
- package/docs/RUNTIME-COMPAT-MATRIX.md +139 -175
- package/docs/SKILL-AUTHORING.md +3 -3
- package/docs/STATE-DB-ACCESS-POLICY.md +13 -8
- package/docs/WORKFLOW-LIFECYCLE.md +174 -448
- package/hooks/README.md +11 -8
- package/hooks/active_task_context.py +16 -16
- package/hooks/adapters/grok.py +134 -0
- package/hooks/{context_spine.py → context_envelope.py} +26 -22
- package/hooks/health_check.py +19 -29
- package/hooks/pre_stop_check.py +22 -12
- package/hooks/subagent_tracker.py +1 -1
- package/hooks/workflow_checkpoint.py +14 -13
- package/hooks/workflow_context.py +11 -12
- package/hooks/workflow_resume.py +14 -13
- package/mcp-server/{breadcrumb.cjs → hook-context.cjs} +7 -9
- package/mcp-server/lib/baseline-workflow.cjs +36 -38
- package/mcp-server/lib/canonical-json.cjs +19 -0
- package/mcp-server/lib/change-workflow.cjs +596 -44
- package/mcp-server/lib/context-envelope.cjs +508 -0
- package/mcp-server/lib/decision-records.cjs +337 -0
- package/mcp-server/lib/doctor.cjs +91 -33
- package/mcp-server/lib/init-project.cjs +78 -39
- package/mcp-server/lib/managed-file-write.cjs +116 -0
- package/mcp-server/lib/plan-checkpoint.cjs +177 -0
- package/mcp-server/lib/project-context.cjs +178 -0
- package/mcp-server/lib/stage-checkpoints.cjs +309 -0
- package/mcp-server/lib/state-db.cjs +155 -24
- package/mcp-server/lib/task-ledger.cjs +522 -13
- package/mcp-server/lib/ultra-facade.cjs +630 -442
- package/mcp-server/lib/worker-packet.cjs +358 -0
- package/mcp-server/server.cjs +200 -1103
- package/orchestrator/daemon.cjs +27 -9
- package/orchestrator/parallel-orchestrator.cjs +111 -76
- package/orchestrator/session-runner.cjs +135 -34
- package/package.json +10 -3
- package/skills/ultra-change/SKILL.md +9 -8
- package/skills/ultra-deliver/SKILL.md +3 -3
- package/skills/ultra-dev/SKILL.md +18 -11
- package/skills/ultra-doctor/SKILL.md +2 -2
- package/skills/ultra-init/SKILL.md +6 -5
- package/skills/ultra-init/references/brownfield-adoption.md +11 -8
- package/skills/ultra-plan/SKILL.md +10 -8
- package/skills/ultra-research/SKILL.md +12 -7
- package/skills/ultra-review/SKILL.md +9 -6
- package/skills/ultra-review/references/unified-schema.md +2 -0
- package/skills/ultra-review/scripts/review_wait.py +8 -0
- package/skills/ultra-status/SKILL.md +2 -2
- package/skills/ultra-test/SKILL.md +4 -2
- package/skills/ultra-think/SKILL.md +7 -7
- package/skills/ultra-think/references/interaction-boundary.md +56 -0
- package/spec/cli-protocol.md +80 -199
- package/spec/mcp-tools.yaml +37 -2804
- package/spec/schemas/state-db.sql +107 -4
- package/spec/schemas/{task-ledger.v1.schema.json → task-ledger.v2.schema.json} +70 -6
- package/ultra-tools/commands/status.cjs +369 -243
- package/mcp-server/lib/context-spine.cjs +0 -1208
- package/mcp-server/lib/decision-dialogue.cjs +0 -1128
- package/mcp-server/lib/project-breadcrumb.cjs +0 -173
- package/mcp-server/lib/spec-learning.cjs +0 -284
- package/mcp-server/lib/workflow-state.cjs +0 -3129
- 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
|
|
12
|
-
const
|
|
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
|
|
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 {
|
|
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,
|
|
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
|
-
:
|
|
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
|
|
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 =
|
|
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, {
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
?
|
|
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 = {}, {
|
|
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 =
|
|
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 (
|
|
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
|
-
&&
|
|
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
|
|
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) =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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(
|
|
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
|
|
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, {
|
|
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
|
-
|
|
1694
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
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,
|