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
package/hooks/workflow_resume.py
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""Re-inject live
|
|
2
|
+
"""Re-inject the live Context Envelope after compaction; checkpoints are advisory."""
|
|
3
3
|
|
|
4
4
|
import json
|
|
5
5
|
import sys
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
|
-
from
|
|
10
|
-
|
|
9
|
+
from context_envelope import (
|
|
10
|
+
ContextEnvelopeError,
|
|
11
11
|
find_root_for_hook,
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
read_context_envelope,
|
|
13
|
+
render_context_envelope,
|
|
14
14
|
)
|
|
15
15
|
|
|
16
16
|
|
|
@@ -35,10 +35,11 @@ def _checkpoint_advisory(root: Path):
|
|
|
35
35
|
return None
|
|
36
36
|
if (
|
|
37
37
|
not isinstance(value, dict)
|
|
38
|
-
or value.get("schema") !=
|
|
39
|
-
or not isinstance(value.get("
|
|
38
|
+
or value.get("schema") != 3
|
|
39
|
+
or not isinstance(value.get("context"), dict)
|
|
40
40
|
or not isinstance(value.get("rendered"), str)
|
|
41
41
|
or not value["rendered"].strip()
|
|
42
|
+
or value.get("context_digest") != value["context"].get("digest")
|
|
42
43
|
):
|
|
43
44
|
print(f"[workflow_resume] invalid checkpoint schema in {file}", file=sys.stderr)
|
|
44
45
|
return None
|
|
@@ -46,7 +47,7 @@ def _checkpoint_advisory(root: Path):
|
|
|
46
47
|
"[Ultra checkpoint advisory]",
|
|
47
48
|
"Live .ultra/.runtime/state.db could not be read; this checkpoint is not authority.",
|
|
48
49
|
value["rendered"],
|
|
49
|
-
"Run ultra
|
|
50
|
+
"Run ultra.doctor before mutation.",
|
|
50
51
|
])
|
|
51
52
|
|
|
52
53
|
|
|
@@ -66,11 +67,11 @@ def main() -> None:
|
|
|
66
67
|
return
|
|
67
68
|
text = None
|
|
68
69
|
try:
|
|
69
|
-
|
|
70
|
-
if
|
|
71
|
-
text =
|
|
72
|
-
except
|
|
73
|
-
print(f"[workflow_resume] cannot inspect Context
|
|
70
|
+
envelope = read_context_envelope(root)
|
|
71
|
+
if envelope:
|
|
72
|
+
text = render_context_envelope(root, envelope)
|
|
73
|
+
except ContextEnvelopeError as exc:
|
|
74
|
+
print(f"[workflow_resume] cannot inspect Context Envelope: {exc}", file=sys.stderr)
|
|
74
75
|
text = _checkpoint_advisory(root)
|
|
75
76
|
if not text:
|
|
76
77
|
print(json.dumps({}))
|
|
@@ -13,18 +13,16 @@ function main() {
|
|
|
13
13
|
const requested = path.resolve(process.argv[discover ? 3 : 2] || process.cwd());
|
|
14
14
|
const rootDir = discover ? runtime.findProjectRoot(requested) : requested;
|
|
15
15
|
if (!rootDir) {
|
|
16
|
-
process.stdout.write(`${JSON.stringify({
|
|
17
|
-
root: null,
|
|
18
|
-
breadcrumb: null,
|
|
19
|
-
text: null,
|
|
20
|
-
})}\n`);
|
|
16
|
+
process.stdout.write(`${JSON.stringify({ root: null, context: null, text: null })}\n`);
|
|
21
17
|
return;
|
|
22
18
|
}
|
|
23
|
-
const
|
|
19
|
+
const context = runtime.readProjectContextEnvelope(rootDir, {
|
|
20
|
+
runtime: process.env.UBP_HOOK_RUNTIME || 'hook',
|
|
21
|
+
});
|
|
24
22
|
process.stdout.write(`${JSON.stringify({
|
|
25
23
|
root: rootDir,
|
|
26
|
-
|
|
27
|
-
text:
|
|
24
|
+
context,
|
|
25
|
+
text: context ? runtime.renderProjectContextEnvelope(rootDir, context) : null,
|
|
28
26
|
})}\n`);
|
|
29
27
|
}
|
|
30
28
|
|
|
@@ -32,7 +30,7 @@ if (require.main === module) {
|
|
|
32
30
|
try {
|
|
33
31
|
main();
|
|
34
32
|
} catch (error) {
|
|
35
|
-
process.stderr.write(`${error.code || '
|
|
33
|
+
process.stderr.write(`${error.code || 'CONTEXT_ENVELOPE_FAILED'}: ${error.message}\n`);
|
|
36
34
|
process.exitCode = 2;
|
|
37
35
|
}
|
|
38
36
|
}
|
|
@@ -7,7 +7,7 @@ const { spawnSync } = require('node:child_process');
|
|
|
7
7
|
|
|
8
8
|
const ops = require('./state-ops.cjs');
|
|
9
9
|
const providerRefs = require('./provider-refs.cjs');
|
|
10
|
-
const
|
|
10
|
+
const checkpoints = require('./stage-checkpoints.cjs');
|
|
11
11
|
|
|
12
12
|
const BASELINE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
13
13
|
const MODES = new Set(['greenfield', 'brownfield']);
|
|
@@ -660,27 +660,10 @@ function storedReadyInvariantBlockers(baseline) {
|
|
|
660
660
|
if (!Array.isArray(baseline.scope) || baseline.scope.length === 0) {
|
|
661
661
|
blockers.push('BASELINE_SCOPE_MISSING');
|
|
662
662
|
}
|
|
663
|
-
if (Array.isArray(baseline.
|
|
664
|
-
for (const kind of ['discovery', 'product', 'architecture']) {
|
|
665
|
-
if (!baseline.spec_refs.some((item) => item?.kind === kind)) {
|
|
666
|
-
blockers.push(`BASELINE_SPEC_MISSING:${kind}`);
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
if (!Array.isArray(baseline.evidence) || baseline.evidence.length === 0) {
|
|
671
|
-
blockers.push('BASELINE_EVIDENCE_MISSING');
|
|
672
|
-
} else if (baseline.mode === 'brownfield'
|
|
673
|
-
&& !baseline.evidence.some((item) => item?.kind === 'source')) {
|
|
674
|
-
blockers.push('BASELINE_SOURCE_EVIDENCE_MISSING');
|
|
675
|
-
}
|
|
676
|
-
if (!Array.isArray(baseline.verification) || baseline.verification.length === 0) {
|
|
677
|
-
blockers.push('BASELINE_VERIFICATION_MISSING');
|
|
678
|
-
} else {
|
|
663
|
+
if (Array.isArray(baseline.verification)) {
|
|
679
664
|
for (const item of baseline.verification) {
|
|
680
665
|
if (!item || typeof item !== 'object') {
|
|
681
666
|
blockers.push('BASELINE_STATE_INVALID:verification');
|
|
682
|
-
} else if (item.status === 'not_run') {
|
|
683
|
-
blockers.push(`BASELINE_VERIFICATION_NOT_RUN:${item.name || 'unknown'}`);
|
|
684
667
|
} else if (item.status === 'known_red') {
|
|
685
668
|
if (!String(item.rationale || '').trim()) {
|
|
686
669
|
blockers.push(`BASELINE_KNOWN_RED_RATIONALE_MISSING:${item.name || 'unknown'}`);
|
|
@@ -688,7 +671,7 @@ function storedReadyInvariantBlockers(baseline) {
|
|
|
688
671
|
if (!baseline.known_red_accepted) {
|
|
689
672
|
blockers.push(`BASELINE_KNOWN_RED_NOT_ACCEPTED:${item.name || 'unknown'}`);
|
|
690
673
|
}
|
|
691
|
-
} else if (item.status
|
|
674
|
+
} else if (!['pass', 'not_run'].includes(item.status)) {
|
|
692
675
|
blockers.push(`BASELINE_VERIFICATION_STATUS_INVALID:${item.name || 'unknown'}`);
|
|
693
676
|
}
|
|
694
677
|
}
|
|
@@ -697,68 +680,103 @@ function storedReadyInvariantBlockers(baseline) {
|
|
|
697
680
|
blockers.push('BASELINE_REVISION_MISSING');
|
|
698
681
|
}
|
|
699
682
|
if (!String(baseline.approved_by || '').trim()) blockers.push('BASELINE_APPROVER_MISSING');
|
|
700
|
-
if (String(baseline.approval_note || '').trim()
|
|
683
|
+
if (!String(baseline.approval_note || '').trim()) {
|
|
701
684
|
blockers.push('BASELINE_APPROVAL_NOTE_MISSING');
|
|
702
685
|
}
|
|
703
686
|
if (!baseline.converged_at || Number.isNaN(Date.parse(baseline.converged_at))) {
|
|
704
687
|
blockers.push('BASELINE_CONVERGENCE_TIMESTAMP_MISSING');
|
|
705
688
|
}
|
|
689
|
+
return [...new Set(blockers)];
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function storedReadySemanticWarnings(baseline) {
|
|
693
|
+
const warnings = [];
|
|
694
|
+
if (Array.isArray(baseline.spec_refs)) {
|
|
695
|
+
for (const kind of ['discovery', 'product', 'architecture']) {
|
|
696
|
+
if (!baseline.spec_refs.some((item) => item?.kind === kind)) {
|
|
697
|
+
warnings.push(`BASELINE_SPEC_MISSING:${kind}`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
if (Array.isArray(baseline.evidence) && baseline.evidence.length === 0) {
|
|
702
|
+
warnings.push('BASELINE_EVIDENCE_MISSING');
|
|
703
|
+
}
|
|
704
|
+
if (baseline.mode === 'brownfield'
|
|
705
|
+
&& Array.isArray(baseline.evidence)
|
|
706
|
+
&& !baseline.evidence.some((item) => item?.kind === 'source')) {
|
|
707
|
+
warnings.push('BASELINE_SOURCE_EVIDENCE_MISSING');
|
|
708
|
+
}
|
|
709
|
+
if (Array.isArray(baseline.verification) && baseline.verification.length === 0) {
|
|
710
|
+
warnings.push('BASELINE_VERIFICATION_MISSING');
|
|
711
|
+
} else if (Array.isArray(baseline.verification)) {
|
|
712
|
+
for (const item of baseline.verification) {
|
|
713
|
+
if (item?.status === 'not_run') {
|
|
714
|
+
warnings.push(`BASELINE_VERIFICATION_NOT_RUN:${item.name || 'unknown'}`);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
}
|
|
706
718
|
if (Array.isArray(baseline.unknowns)) {
|
|
707
719
|
for (const item of baseline.unknowns.filter((unknown) => unknown?.blocking === true)) {
|
|
708
|
-
|
|
720
|
+
warnings.push(`BASELINE_UNKNOWN_RECORDED:${item.summary || 'unknown'}`);
|
|
709
721
|
}
|
|
710
722
|
}
|
|
711
723
|
if (Array.isArray(baseline.gaps)) {
|
|
712
724
|
for (const gap of baseline.gaps.filter((item) => (
|
|
713
725
|
item?.status === 'open' && item?.blocking === true
|
|
714
726
|
))) {
|
|
715
|
-
|
|
727
|
+
warnings.push(`BASELINE_GAP_RECORDED:${gap.id || 'unknown'}`);
|
|
716
728
|
}
|
|
717
729
|
}
|
|
718
|
-
return [...new Set(
|
|
730
|
+
return [...new Set(warnings)];
|
|
719
731
|
}
|
|
720
732
|
|
|
721
|
-
function
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
)
|
|
728
|
-
const row = rows.find((candidate) => workflows.isConsumableWorkflowAuthority(
|
|
729
|
-
workflows.readWorkflow(db, candidate.id, { rootDir }),
|
|
730
|
-
));
|
|
731
|
-
return row ? workflows.readWorkflow(db, row.id, { rootDir }) : null;
|
|
733
|
+
function acceptedBaselineResearch(db, baseline) {
|
|
734
|
+
return checkpoints.currentCheckpoint(
|
|
735
|
+
db,
|
|
736
|
+
'research',
|
|
737
|
+
{ baseline_id: baseline.id },
|
|
738
|
+
{ includeDraft: false },
|
|
739
|
+
);
|
|
732
740
|
}
|
|
733
741
|
|
|
734
|
-
function convergenceResearchBlockers(db, baseline
|
|
735
|
-
const
|
|
736
|
-
if (!
|
|
737
|
-
return
|
|
742
|
+
function convergenceResearchBlockers(db, baseline) {
|
|
743
|
+
const checkpoint = acceptedBaselineResearch(db, baseline);
|
|
744
|
+
if (!checkpoint) return ['BASELINE_RESEARCH_INCOMPLETE'];
|
|
745
|
+
return [];
|
|
738
746
|
}
|
|
739
747
|
|
|
740
|
-
function storedResearchHealth(db, baseline
|
|
741
|
-
|
|
742
|
-
|
|
748
|
+
function storedResearchHealth(db, baseline) {
|
|
749
|
+
const checkpointId = baseline.research_checkpoint_id || baseline.research_run_id;
|
|
750
|
+
if (!checkpointId) {
|
|
751
|
+
return { blockers: [], warnings: ['BASELINE_RESEARCH_PROVENANCE_MISSING'] };
|
|
752
|
+
}
|
|
753
|
+
let checkpoint;
|
|
754
|
+
try {
|
|
755
|
+
checkpoint = checkpoints.readCheckpoint(db, checkpointId);
|
|
756
|
+
} catch (error) {
|
|
757
|
+
if (error?.code === 'CHECKPOINT_DIGEST_MISMATCH') {
|
|
758
|
+
return { blockers: ['BASELINE_RESEARCH_RECORD_INVALID'], warnings: [] };
|
|
759
|
+
}
|
|
760
|
+
throw error;
|
|
743
761
|
}
|
|
744
|
-
|
|
745
|
-
|
|
762
|
+
if (!checkpoint
|
|
763
|
+
|| checkpoint.stage !== 'research'
|
|
764
|
+
|| checkpoint.scope_type !== 'baseline'
|
|
765
|
+
|| checkpoint.scope_id !== baseline.id
|
|
766
|
+
|| checkpoints.checkpointDigest(checkpoint) !== checkpoint.digest) {
|
|
746
767
|
return { blockers: ['BASELINE_RESEARCH_RECORD_INVALID'], warnings: [] };
|
|
747
768
|
}
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
}
|
|
758
|
-
return {
|
|
759
|
-
blockers: blockers.map((item) => `BASELINE_RESEARCH_${item}`),
|
|
760
|
-
warnings: [],
|
|
761
|
-
};
|
|
769
|
+
if (checkpoint.status !== 'accepted') {
|
|
770
|
+
return {
|
|
771
|
+
blockers: [],
|
|
772
|
+
warnings: [
|
|
773
|
+
checkpoint.status === 'superseded'
|
|
774
|
+
? 'BASELINE_RESEARCH_SUPERSEDED'
|
|
775
|
+
: `BASELINE_RESEARCH_NOT_ACCEPTED:${checkpoint.status}`,
|
|
776
|
+
],
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
return { blockers: [], warnings: checkpoint.diagnostics || [] };
|
|
762
780
|
}
|
|
763
781
|
|
|
764
782
|
function convergenceBlockers(db, baseline, input, rootDir) {
|
|
@@ -777,10 +795,6 @@ function convergenceBlockers(db, baseline, input, rootDir) {
|
|
|
777
795
|
if (baseline.repository_branch && snapshot.branch
|
|
778
796
|
&& baseline.repository_branch !== snapshot.branch) blockers.add('BASELINE_BRANCH_STALE');
|
|
779
797
|
if (!Array.isArray(baseline.scope) || baseline.scope.length === 0) blockers.add('BASELINE_SCOPE_MISSING');
|
|
780
|
-
for (const kind of ['discovery', 'product', 'architecture']) {
|
|
781
|
-
if (!baseline.spec_refs.some((ref) => ref.kind === kind)) blockers.add(`BASELINE_SPEC_MISSING:${kind}`);
|
|
782
|
-
}
|
|
783
|
-
for (const blocker of convergenceResearchBlockers(db, baseline, rootDir)) blockers.add(blocker);
|
|
784
798
|
for (const blocker of storedSpecBlockers(baseline, rootDir)) blockers.add(blocker);
|
|
785
799
|
for (const blocker of storedEvidenceBlockers(baseline, rootDir)) blockers.add(blocker);
|
|
786
800
|
for (const blocker of storedScopeBlockers(baseline, rootDir)) blockers.add(blocker);
|
|
@@ -791,29 +805,23 @@ function convergenceBlockers(db, baseline, input, rootDir) {
|
|
|
791
805
|
if (baseline.worktree_state === 'dirty' && input.accept_dirty_worktree !== true) {
|
|
792
806
|
blockers.add('BASELINE_DIRTY_WORKTREE_NOT_ACCEPTED');
|
|
793
807
|
}
|
|
794
|
-
if (baseline.evidence.length === 0) blockers.add('BASELINE_EVIDENCE_MISSING');
|
|
795
|
-
if (baseline.mode === 'brownfield' && !baseline.evidence.some((item) => item.kind === 'source')) {
|
|
796
|
-
blockers.add('BASELINE_SOURCE_EVIDENCE_MISSING');
|
|
797
|
-
}
|
|
798
|
-
if (baseline.verification.length === 0) blockers.add('BASELINE_VERIFICATION_MISSING');
|
|
799
808
|
for (const item of baseline.verification) {
|
|
800
|
-
if (item.status === 'not_run') blockers.add(`BASELINE_VERIFICATION_NOT_RUN:${item.name}`);
|
|
801
809
|
if (item.status === 'known_red') {
|
|
802
810
|
if (input.accept_known_red !== true) blockers.add(`BASELINE_KNOWN_RED_NOT_ACCEPTED:${item.name}`);
|
|
803
811
|
if (!item.rationale) blockers.add(`BASELINE_KNOWN_RED_RATIONALE_MISSING:${item.name}`);
|
|
804
812
|
}
|
|
805
813
|
}
|
|
806
|
-
for (const item of baseline.unknowns.filter((unknown) => unknown.blocking)) {
|
|
807
|
-
blockers.add(`BASELINE_UNKNOWN_BLOCKING:${item.summary}`);
|
|
808
|
-
}
|
|
809
|
-
for (const gap of baseline.gaps.filter((item) => item.status === 'open' && item.blocking)) {
|
|
810
|
-
blockers.add(`BASELINE_GAP_BLOCKING:${gap.id}`);
|
|
811
|
-
}
|
|
812
814
|
if (!String(input.approved_by || '').trim()) blockers.add('BASELINE_APPROVER_REQUIRED');
|
|
813
|
-
if (String(input.approval_note || '').trim()
|
|
815
|
+
if (!String(input.approval_note || '').trim()) blockers.add('BASELINE_APPROVAL_NOTE_REQUIRED');
|
|
814
816
|
return [...blockers].sort();
|
|
815
817
|
}
|
|
816
818
|
|
|
819
|
+
function convergenceWarnings(db, baseline) {
|
|
820
|
+
const warnings = new Set(storedReadySemanticWarnings(baseline));
|
|
821
|
+
for (const warning of convergenceResearchBlockers(db, baseline)) warnings.add(warning);
|
|
822
|
+
return [...warnings].sort();
|
|
823
|
+
}
|
|
824
|
+
|
|
817
825
|
function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent = true } = {}) {
|
|
818
826
|
const current = readBaseline(db, input.id);
|
|
819
827
|
if (!current) throw new BaselineWorkflowError('BASELINE_NOT_FOUND', `baseline ${input.id} not found`);
|
|
@@ -821,6 +829,7 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
|
|
|
821
829
|
throw new BaselineWorkflowError('BASELINE_NOT_CONVERGEABLE', `baseline ${current.id} is ${current.status}`);
|
|
822
830
|
}
|
|
823
831
|
const blockers = convergenceBlockers(db, current, input, rootDir);
|
|
832
|
+
const warnings = convergenceWarnings(db, current);
|
|
824
833
|
const ts = nowIso();
|
|
825
834
|
if (blockers.length > 0) {
|
|
826
835
|
ops.tx(db, () => {
|
|
@@ -832,18 +841,25 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
|
|
|
832
841
|
});
|
|
833
842
|
}
|
|
834
843
|
});
|
|
835
|
-
return {
|
|
844
|
+
return {
|
|
845
|
+
ready: false,
|
|
846
|
+
status: 'blocked',
|
|
847
|
+
blockers,
|
|
848
|
+
warnings,
|
|
849
|
+
baseline: readBaseline(db, current.id),
|
|
850
|
+
};
|
|
836
851
|
}
|
|
837
|
-
const research =
|
|
852
|
+
const research = acceptedBaselineResearch(db, current);
|
|
838
853
|
ops.tx(db, () => {
|
|
839
854
|
db.prepare(
|
|
840
855
|
`UPDATE baselines SET status = 'ready', approved_by = ?, approval_note = ?,
|
|
841
|
-
worktree_accepted = ?, known_red_accepted = ?,
|
|
856
|
+
worktree_accepted = ?, known_red_accepted = ?, research_checkpoint_id = ?,
|
|
857
|
+
research_run_id = NULL,
|
|
842
858
|
converged_at = ?, updated_at = ? WHERE id = ?`,
|
|
843
859
|
).run(
|
|
844
860
|
String(input.approved_by).trim(), String(input.approval_note).trim(),
|
|
845
861
|
input.accept_dirty_worktree === true ? 1 : 0,
|
|
846
|
-
input.accept_known_red === true ? 1 : 0, research
|
|
862
|
+
input.accept_known_red === true ? 1 : 0, research?.id || null, ts, ts, current.id,
|
|
847
863
|
);
|
|
848
864
|
if (emitEvent) {
|
|
849
865
|
ops.appendEventInTx(db, {
|
|
@@ -851,7 +867,13 @@ function convergeBaseline(db, input = {}, { rootDir = process.cwd(), emitEvent =
|
|
|
851
867
|
});
|
|
852
868
|
}
|
|
853
869
|
});
|
|
854
|
-
return {
|
|
870
|
+
return {
|
|
871
|
+
ready: true,
|
|
872
|
+
status: 'ready',
|
|
873
|
+
blockers: [],
|
|
874
|
+
warnings,
|
|
875
|
+
baseline: readBaseline(db, current.id),
|
|
876
|
+
};
|
|
855
877
|
}
|
|
856
878
|
|
|
857
879
|
function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
|
|
@@ -874,7 +896,7 @@ function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
|
|
|
874
896
|
status: 'fail', blockers: [`BASELINE_NOT_READY:${baseline.status}`], warnings: [], baseline,
|
|
875
897
|
};
|
|
876
898
|
}
|
|
877
|
-
const research = storedResearchHealth(db, baseline
|
|
899
|
+
const research = storedResearchHealth(db, baseline);
|
|
878
900
|
const blockers = [
|
|
879
901
|
...storedReadyInvariantBlockers(baseline),
|
|
880
902
|
...(Array.isArray(baseline.scope) ? storedScopeBlockers(baseline, rootDir) : []),
|
|
@@ -882,6 +904,10 @@ function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
|
|
|
882
904
|
...(Array.isArray(baseline.evidence) ? storedEvidenceBlockers(baseline, rootDir) : []),
|
|
883
905
|
...research.blockers,
|
|
884
906
|
];
|
|
907
|
+
const warnings = [
|
|
908
|
+
...storedReadySemanticWarnings(baseline),
|
|
909
|
+
...research.warnings,
|
|
910
|
+
];
|
|
885
911
|
const snapshot = gitWorktreeSnapshot(
|
|
886
912
|
rootDir, Array.isArray(baseline.scope) && baseline.scope.length > 0 ? baseline.scope : ['.'],
|
|
887
913
|
);
|
|
@@ -911,7 +937,7 @@ function inspectBaseline(db, { rootDir = process.cwd(), id } = {}) {
|
|
|
911
937
|
}
|
|
912
938
|
return {
|
|
913
939
|
status: blockers.length === 0 ? 'pass' : 'fail', blockers: [...new Set(blockers)].sort(),
|
|
914
|
-
warnings:
|
|
940
|
+
warnings: [...new Set(warnings)], baseline,
|
|
915
941
|
};
|
|
916
942
|
}
|
|
917
943
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
function canonicalJson(value) {
|
|
6
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
7
|
+
if (value && typeof value === 'object') {
|
|
8
|
+
return `{${Object.keys(value).sort().map(
|
|
9
|
+
(key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`,
|
|
10
|
+
).join(',')}}`;
|
|
11
|
+
}
|
|
12
|
+
return JSON.stringify(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function digest(value) {
|
|
16
|
+
return crypto.createHash('sha256').update(canonicalJson(value)).digest('hex');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
module.exports = { canonicalJson, digest };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
|
|
5
|
+
function parseJson(value, fallback = {}) {
|
|
6
|
+
if (typeof value !== 'string' || !value.trim()) return fallback;
|
|
7
|
+
try { return JSON.parse(value); } catch { return fallback; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function changeStateDigest(change) {
|
|
11
|
+
if (!change) return null;
|
|
12
|
+
const payload = {
|
|
13
|
+
id: change.id,
|
|
14
|
+
kind: change.kind,
|
|
15
|
+
intent: change.intent,
|
|
16
|
+
docs_impact: change.docs_impact || parseJson(change.docs_impact_json, {}),
|
|
17
|
+
provider_refs: change.provider_refs || parseJson(change.provider_refs_json, {}),
|
|
18
|
+
contract: change.contract || parseJson(change.contract_json, {}),
|
|
19
|
+
classification: change.classification || parseJson(change.classification_json, {}),
|
|
20
|
+
research_disposition: change.research_disposition
|
|
21
|
+
|| parseJson(change.research_disposition_json, {}),
|
|
22
|
+
};
|
|
23
|
+
return crypto.createHash('sha256').update(JSON.stringify(payload)).digest('hex');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { changeStateDigest };
|