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