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