ultra-builder-pro-cli 0.16.0 → 0.17.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/specs/research-distillate.md +4 -3
- package/CHANGELOG.md +86 -0
- package/README.md +54 -24
- package/adapters/_shared/codex-assets.cjs +10 -5
- package/adapters/_shared/handbook.cjs +1 -1
- package/adapters/_shared/interaction-contract.cjs +61 -0
- package/adapters/claude.js +1 -1
- package/adapters/opencode.js +1 -1
- package/bin/orchestrator.js +175 -19
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-deliver.md +3 -3
- package/commands/ultra-init.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +2 -2
- package/commands/ultra-review.md +10 -0
- package/commands/ultra-status.md +1 -1
- package/commands/ultra-test.md +1 -1
- package/docs/AGENT-CONTEXT.md +11 -8
- package/docs/ARCHITECTURE.md +67 -9
- package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
- package/docs/PLAN.zh-CN.md +19 -13
- package/docs/RUNTIME-COMPAT-MATRIX.md +2 -2
- package/docs/WORKFLOW-LIFECYCLE.md +123 -59
- package/hooks/pre_stop_check.py +3 -2
- package/mcp-server/lib/baseline-workflow.cjs +84 -16
- package/mcp-server/lib/change-workflow.cjs +55 -35
- package/mcp-server/lib/context-spine.cjs +171 -105
- package/mcp-server/lib/git-bootstrap.cjs +240 -0
- package/mcp-server/lib/init-project.cjs +178 -41
- package/mcp-server/lib/project-breadcrumb.cjs +14 -14
- package/mcp-server/lib/state-db.cjs +257 -10
- package/mcp-server/lib/state-ops.cjs +64 -1
- package/mcp-server/lib/workflow-state.cjs +300 -205
- package/mcp-server/server.cjs +58 -34
- package/orchestrator/auto-merge.cjs +19 -0
- package/orchestrator/daemon.cjs +104 -0
- package/orchestrator/parallel-orchestrator.cjs +371 -49
- package/orchestrator/session-runner.cjs +354 -25
- package/package.json +2 -1
- package/skills/ultra-change/SKILL.md +69 -94
- package/skills/ultra-deliver/SKILL.md +37 -65
- package/skills/ultra-dev/SKILL.md +50 -52
- package/skills/ultra-doctor/SKILL.md +34 -49
- package/skills/ultra-init/SKILL.md +78 -105
- package/skills/ultra-plan/SKILL.md +78 -99
- package/skills/ultra-research/SKILL.md +99 -113
- package/skills/ultra-review/SKILL.md +38 -53
- package/skills/ultra-review/references/review-modes.md +5 -3
- package/skills/ultra-review/references/unified-schema.md +26 -6
- package/skills/ultra-review/scripts/review_wait.py +12 -0
- package/skills/ultra-status/SKILL.md +34 -44
- package/skills/ultra-test/SKILL.md +61 -54
- package/skills/ultra-think/SKILL.md +37 -51
- package/skills/ultra-think/references/decision-dialogue.md +87 -111
- package/spec/cli-protocol.md +35 -0
- package/spec/mcp-tools.yaml +145 -28
- package/spec/schemas/delivery-report.v1.schema.json +1 -46
- package/spec/schemas/state-db.sql +8 -2
- package/spec/schemas/test-report.v1.schema.json +45 -14
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/session.cjs +10 -3
- package/ultra-tools/commands/status.cjs +19 -14
- package/ultra-tools/commands/system.cjs +2 -2
- package/ultra-tools/commands/task.cjs +3 -0
|
@@ -12,7 +12,7 @@ const decisionDialogue = require('./decision-dialogue.cjs');
|
|
|
12
12
|
const testReportSchema = require('../../spec/schemas/test-report.v1.schema.json');
|
|
13
13
|
const deliveryReportSchema = require('../../spec/schemas/delivery-report.v1.schema.json');
|
|
14
14
|
|
|
15
|
-
const DEFINITION_VERSION = '
|
|
15
|
+
const DEFINITION_VERSION = '2.0';
|
|
16
16
|
const RUN_KINDS = new Set(['init', 'research', 'plan', 'change', 'dev', 'test', 'review', 'deliver']);
|
|
17
17
|
const RUN_STATUSES = new Set(['active', 'blocked', 'ready', 'completed', 'cancelled']);
|
|
18
18
|
const STEP_STATUSES = new Set(['pending', 'in_progress', 'completed', 'skipped', 'blocked']);
|
|
@@ -33,8 +33,7 @@ const WORKFLOW_DEFINITIONS = Object.freeze({
|
|
|
33
33
|
step('inspect-authority', 'Inspect repository and existing Ultra authority'),
|
|
34
34
|
step('classify-repository', 'Classify greenfield, brownfield, or migrated state'),
|
|
35
35
|
step('scaffold-authority', 'Create or resume the authoritative state and scaffold'),
|
|
36
|
-
step('
|
|
37
|
-
step('verify-initialization', 'Verify authority, projections, and the next route', { evidence_required: true }),
|
|
36
|
+
step('verify-initialization', 'Verify authority, projections, and the initialization result', { evidence_required: true }),
|
|
38
37
|
]),
|
|
39
38
|
research: Object.freeze([
|
|
40
39
|
step('00-problem-validation', 'Validate the problem and demand evidence', { evidence_required: true, output_required: true }),
|
|
@@ -57,12 +56,10 @@ const WORKFLOW_DEFINITIONS = Object.freeze({
|
|
|
57
56
|
]),
|
|
58
57
|
plan: Object.freeze([
|
|
59
58
|
step('validate-baseline', 'Validate baseline and research coverage', { evidence_required: true }),
|
|
60
|
-
step('select-posture', 'Record the owner-selected planning posture', { evidence_required: true }),
|
|
61
59
|
step('analyze-requirements', 'Trace requirements and acceptance'),
|
|
62
60
|
step('analyze-codebase', 'Inspect current codebase patterns and boundaries', { evidence_required: true }),
|
|
63
61
|
step('design-slices', 'Design walking skeleton and vertical slices'),
|
|
64
62
|
step('validate-dependencies', 'Validate ownership, dependencies, and parallel waves', { evidence_required: true }),
|
|
65
|
-
step('approve-plan', 'Obtain explicit plan approval', { evidence_required: true }),
|
|
66
63
|
step('persist-task-contracts', 'Persist tasks and complete execution contracts', { evidence_required: true }),
|
|
67
64
|
step('verify-plan', 'Read back and verify the authoritative plan', {
|
|
68
65
|
evidence_required: true, output_required: true,
|
|
@@ -72,9 +69,6 @@ const WORKFLOW_DEFINITIONS = Object.freeze({
|
|
|
72
69
|
step('bind-baseline', 'Bind current baseline and repository revision', { evidence_required: true }),
|
|
73
70
|
step('classify-change', 'Classify quick, standard, major, or incident'),
|
|
74
71
|
step('record-intent', 'Record intent, scope, acceptance, and documentation impact', { evidence_required: true }),
|
|
75
|
-
step('plan-change', 'Create or validate change tasks'),
|
|
76
|
-
step('compile-context', 'Compile bounded role context', { evidence_required: true, output_required: true }),
|
|
77
|
-
step('verify-readiness', 'Verify one executable next action', { evidence_required: true }),
|
|
78
72
|
]),
|
|
79
73
|
dev: Object.freeze([
|
|
80
74
|
step('bind-task', 'Bind the authoritative task and dependencies', { evidence_required: true }),
|
|
@@ -89,10 +83,10 @@ const WORKFLOW_DEFINITIONS = Object.freeze({
|
|
|
89
83
|
step('bind-scope', 'Bind completed tasks and current revision', { evidence_required: true }),
|
|
90
84
|
step('compile-context', 'Compile independent checking context', { evidence_required: true, output_required: true }),
|
|
91
85
|
step('map-acceptance', 'Map acceptance claims to executable checks'),
|
|
92
|
-
step('execute-checks', 'Execute
|
|
86
|
+
step('execute-checks', 'Execute the risk-selected verification profile', { evidence_required: true }),
|
|
93
87
|
step('verify-public-seam', 'Verify the declared public seam', { evidence_required: true }),
|
|
94
88
|
step('write-report', 'Write the current test evidence report', { evidence_required: true, output_required: true }),
|
|
95
|
-
step('verify-test-gate', 'Verify report freshness and
|
|
89
|
+
step('verify-test-gate', 'Verify report freshness and expose valid transitions', { evidence_required: true }),
|
|
96
90
|
]),
|
|
97
91
|
review: Object.freeze([
|
|
98
92
|
step('bind-diff', 'Bind one current diff and acceptance scope', { evidence_required: true }),
|
|
@@ -105,14 +99,17 @@ const WORKFLOW_DEFINITIONS = Object.freeze({
|
|
|
105
99
|
deliver: Object.freeze([
|
|
106
100
|
step('bind-evidence', 'Bind current test, review, task, and revision evidence', { evidence_required: true }),
|
|
107
101
|
step('reconcile-specifications', 'Resolve learning and reconcile specifications', { evidence_required: true }),
|
|
108
|
-
step('verify-candidate', 'Verify the
|
|
102
|
+
step('verify-candidate', 'Verify the local delivery candidate and recovery path', { evidence_required: true, output_required: true }),
|
|
109
103
|
step('converge-authority', 'Converge baseline or change authority', { evidence_required: true }),
|
|
110
104
|
step('archive-change', 'Archive the converged change when applicable', { evidence_required: true }),
|
|
111
|
-
step('
|
|
112
|
-
step('verify-delivery', 'Verify local and remote delivery evidence', { evidence_required: true, output_required: true }),
|
|
105
|
+
step('verify-delivery', 'Verify local delivery and recovery evidence', { evidence_required: true, output_required: true }),
|
|
113
106
|
]),
|
|
114
107
|
});
|
|
115
108
|
|
|
109
|
+
const RESEARCH_DISPOSITIONS = new Set([
|
|
110
|
+
'execute', 'verify_existing', 'reuse', 'not_applicable', 'deferred',
|
|
111
|
+
]);
|
|
112
|
+
|
|
116
113
|
const RESEARCH_MODES = Object.freeze({
|
|
117
114
|
full: WORKFLOW_DEFINITIONS.research.map((item) => item.id),
|
|
118
115
|
adoption: WORKFLOW_DEFINITIONS.research.map((item) => item.id),
|
|
@@ -380,32 +377,6 @@ function validateSynthesisTrace(db, runRow, semanticRecords) {
|
|
|
380
377
|
}
|
|
381
378
|
}
|
|
382
379
|
|
|
383
|
-
function validateReleaseAuthorizationDecision(decisions) {
|
|
384
|
-
if (!Array.isArray(decisions) || decisions.length !== 1) {
|
|
385
|
-
throw new WorkflowStateError(
|
|
386
|
-
'WORKFLOW_RELEASE_AUTHORIZATION_INVALID',
|
|
387
|
-
'release-if-authorized requires exactly one release_authorization decision',
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
const decision = decisions[0];
|
|
391
|
-
if (decision?.kind !== 'release_authorization'
|
|
392
|
-
|| typeof decision.authorized !== 'boolean'
|
|
393
|
-
|| !String(decision.reason || '').trim()) {
|
|
394
|
-
throw new WorkflowStateError(
|
|
395
|
-
'WORKFLOW_RELEASE_AUTHORIZATION_INVALID',
|
|
396
|
-
'release authorization requires kind, authorized, and reason',
|
|
397
|
-
);
|
|
398
|
-
}
|
|
399
|
-
if (decision.authorized === true
|
|
400
|
-
&& (!String(decision.approved_by || '').trim() || !String(decision.scope || '').trim())) {
|
|
401
|
-
throw new WorkflowStateError(
|
|
402
|
-
'WORKFLOW_RELEASE_AUTHORIZATION_INVALID',
|
|
403
|
-
'an authorized release requires approved_by and an explicit release scope',
|
|
404
|
-
);
|
|
405
|
-
}
|
|
406
|
-
return decision;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
380
|
function validateResearchStepReport(run, stepId, outputs, rootDir) {
|
|
410
381
|
const expected = path.join(
|
|
411
382
|
'.ultra', 'docs', 'research', run.id, `${stepId}.md`,
|
|
@@ -602,7 +573,95 @@ function listWorkflows(db, filter = {}, { rootDir = process.cwd() } = {}) {
|
|
|
602
573
|
return rows.map((row) => decorateRun(db, row, rootDir));
|
|
603
574
|
}
|
|
604
575
|
|
|
605
|
-
function
|
|
576
|
+
function normalizeResearchCoverage(mode, coverage, selectedSteps, metadata = {}) {
|
|
577
|
+
const knownSteps = WORKFLOW_DEFINITIONS.research.map((item) => item.id);
|
|
578
|
+
const known = new Set(knownSteps);
|
|
579
|
+
if (coverage !== undefined) {
|
|
580
|
+
if (!Array.isArray(coverage) || coverage.length === 0) {
|
|
581
|
+
throw new WorkflowStateError(
|
|
582
|
+
'WORKFLOW_RESEARCH_COVERAGE_REQUIRED',
|
|
583
|
+
'research coverage must be a non-empty array of semantic dispositions',
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
const seen = new Set();
|
|
587
|
+
const normalized = coverage.map((item) => {
|
|
588
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) {
|
|
589
|
+
throw new WorkflowStateError('VALIDATION_ERROR', 'research coverage entries must be objects');
|
|
590
|
+
}
|
|
591
|
+
const stepId = String(item.step_id || '').trim();
|
|
592
|
+
const disposition = String(item.disposition || '').trim();
|
|
593
|
+
const rationale = String(item.rationale || '').trim();
|
|
594
|
+
const evidenceRefs = Array.isArray(item.evidence_refs)
|
|
595
|
+
? [...new Set(item.evidence_refs.map((ref) => String(ref).trim()).filter(Boolean))]
|
|
596
|
+
: [];
|
|
597
|
+
if (!known.has(stepId)) {
|
|
598
|
+
throw new WorkflowStateError('VALIDATION_ERROR', `unknown research step: ${stepId || '(missing)'}`);
|
|
599
|
+
}
|
|
600
|
+
if (seen.has(stepId)) {
|
|
601
|
+
throw new WorkflowStateError('VALIDATION_ERROR', `duplicate research coverage: ${stepId}`);
|
|
602
|
+
}
|
|
603
|
+
if (!RESEARCH_DISPOSITIONS.has(disposition)) {
|
|
604
|
+
throw new WorkflowStateError(
|
|
605
|
+
'VALIDATION_ERROR',
|
|
606
|
+
`unsupported research disposition for ${stepId}: ${disposition || '(missing)'}`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
if (rationale.length < 3) {
|
|
610
|
+
throw new WorkflowStateError(
|
|
611
|
+
'VALIDATION_ERROR', `research coverage ${stepId} requires a rationale`,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
if (['verify_existing', 'reuse', 'not_applicable'].includes(disposition)
|
|
615
|
+
&& evidenceRefs.length === 0) {
|
|
616
|
+
throw new WorkflowStateError(
|
|
617
|
+
'WORKFLOW_RESEARCH_COVERAGE_EVIDENCE_REQUIRED',
|
|
618
|
+
`${stepId} ${disposition} requires at least one evidence reference`,
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
const acceptedBy = item.accepted_by === undefined ? null : String(item.accepted_by).trim();
|
|
622
|
+
const consequence = item.consequence === undefined ? null : String(item.consequence).trim();
|
|
623
|
+
if (disposition === 'deferred' && (!consequence || consequence.length < 3)) {
|
|
624
|
+
throw new WorkflowStateError(
|
|
625
|
+
'VALIDATION_ERROR', `${stepId} deferred coverage requires a consequence`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
seen.add(stepId);
|
|
629
|
+
return {
|
|
630
|
+
step_id: stepId,
|
|
631
|
+
disposition,
|
|
632
|
+
rationale,
|
|
633
|
+
evidence_refs: evidenceRefs,
|
|
634
|
+
...(acceptedBy ? { accepted_by: acceptedBy } : {}),
|
|
635
|
+
...(consequence ? { consequence } : {}),
|
|
636
|
+
};
|
|
637
|
+
});
|
|
638
|
+
if (['full', 'adoption'].includes(mode)) {
|
|
639
|
+
const missing = knownSteps.filter((stepId) => !seen.has(stepId));
|
|
640
|
+
if (missing.length > 0) {
|
|
641
|
+
throw new WorkflowStateError(
|
|
642
|
+
'WORKFLOW_RESEARCH_COVERAGE_REQUIRED',
|
|
643
|
+
`${mode} research must disposition every semantic area: ${missing.join(', ')}`,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
const synthesis = normalized.find((item) => item.step_id === '99-synthesis');
|
|
647
|
+
if (!synthesis || !['execute', 'verify_existing', 'reuse'].includes(synthesis.disposition)) {
|
|
648
|
+
throw new WorkflowStateError(
|
|
649
|
+
'WORKFLOW_RESEARCH_SYNTHESIS_REQUIRED',
|
|
650
|
+
`${mode} research requires an executable or reusable 99-synthesis disposition`,
|
|
651
|
+
);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return normalized.sort(
|
|
655
|
+
(left, right) => knownSteps.indexOf(left.step_id) - knownSteps.indexOf(right.step_id),
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (['full', 'adoption'].includes(mode)) {
|
|
660
|
+
throw new WorkflowStateError(
|
|
661
|
+
'WORKFLOW_RESEARCH_COVERAGE_REQUIRED',
|
|
662
|
+
`${mode} research requires model-selected coverage dispositions`,
|
|
663
|
+
);
|
|
664
|
+
}
|
|
606
665
|
if (mode === 'custom') {
|
|
607
666
|
if (!Array.isArray(selectedSteps) || selectedSteps.length === 0) {
|
|
608
667
|
throw new WorkflowStateError('VALIDATION_ERROR', 'custom research requires selected_steps');
|
|
@@ -610,11 +669,21 @@ function selectedResearchSteps(mode, selectedSteps) {
|
|
|
610
669
|
const known = new Set(WORKFLOW_DEFINITIONS.research.map((item) => item.id));
|
|
611
670
|
const invalid = selectedSteps.find((item) => !known.has(item));
|
|
612
671
|
if (invalid) throw new WorkflowStateError('VALIDATION_ERROR', `unknown research step: ${invalid}`);
|
|
613
|
-
return [...new Set([...selectedSteps, '99-synthesis'])]
|
|
672
|
+
return [...new Set([...selectedSteps, '99-synthesis'])].map((stepId) => ({
|
|
673
|
+
step_id: stepId,
|
|
674
|
+
disposition: 'execute',
|
|
675
|
+
rationale: String(metadata.selection_reason || 'Explicit bounded research selection.').trim(),
|
|
676
|
+
evidence_refs: [],
|
|
677
|
+
}));
|
|
614
678
|
}
|
|
615
679
|
const selected = RESEARCH_MODES[mode];
|
|
616
680
|
if (!selected) throw new WorkflowStateError('VALIDATION_ERROR', `unsupported research mode: ${mode}`);
|
|
617
|
-
return selected
|
|
681
|
+
return selected.map((stepId) => ({
|
|
682
|
+
step_id: stepId,
|
|
683
|
+
disposition: 'execute',
|
|
684
|
+
rationale: String(metadata.selection_reason || `${mode} research profile selected by the host model.`).trim(),
|
|
685
|
+
evidence_refs: [],
|
|
686
|
+
}));
|
|
618
687
|
}
|
|
619
688
|
|
|
620
689
|
function assertReference(db, table, id, field) {
|
|
@@ -632,9 +701,10 @@ function assertWorkflowAuthority(db, input, mode) {
|
|
|
632
701
|
);
|
|
633
702
|
}
|
|
634
703
|
};
|
|
635
|
-
if (input.kind !== 'research'
|
|
704
|
+
if (input.kind !== 'research'
|
|
705
|
+
&& (input.mode !== undefined || input.selected_steps !== undefined || input.coverage !== undefined)) {
|
|
636
706
|
throw new WorkflowStateError(
|
|
637
|
-
'VALIDATION_ERROR', 'mode and
|
|
707
|
+
'VALIDATION_ERROR', 'mode, selected_steps, and coverage are valid only for research workflows',
|
|
638
708
|
);
|
|
639
709
|
}
|
|
640
710
|
if (input.kind === 'init') requireField('baseline_id');
|
|
@@ -732,9 +802,10 @@ function assertPlanWorkflowAuthority(db, input) {
|
|
|
732
802
|
ORDER BY completed_at DESC, rowid DESC LIMIT 1`,
|
|
733
803
|
).get(change.id, disposition.mode);
|
|
734
804
|
const research = row ? readWorkflow(db, row.id, { rootDir: input.root_dir || process.cwd() }) : null;
|
|
735
|
-
const expectedSteps = new Set(
|
|
736
|
-
disposition.
|
|
737
|
-
|
|
805
|
+
const expectedSteps = new Set([
|
|
806
|
+
...(Array.isArray(disposition.selected_steps) ? disposition.selected_steps : []),
|
|
807
|
+
'99-synthesis',
|
|
808
|
+
]);
|
|
738
809
|
const actualSteps = new Set(
|
|
739
810
|
(research?.steps || []).filter((item) => item.required).map((item) => item.step_id),
|
|
740
811
|
);
|
|
@@ -827,6 +898,15 @@ function insertWorkflowInTx(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
|
827
898
|
assertReference(db, 'baselines', input.baseline_id, 'baseline_id');
|
|
828
899
|
assertReference(db, 'changes', input.change_id, 'change_id');
|
|
829
900
|
assertReference(db, 'tasks', input.task_id, 'task_id');
|
|
901
|
+
if (input.task_id && input.change_id) {
|
|
902
|
+
const taskOwner = db.prepare('SELECT change_id FROM tasks WHERE id = ?').get(input.task_id);
|
|
903
|
+
if (taskOwner?.change_id !== input.change_id) {
|
|
904
|
+
throw new WorkflowStateError(
|
|
905
|
+
'TASK_CHANGE_OWNERSHIP_MISMATCH',
|
|
906
|
+
`task ${input.task_id} belongs to ${taskOwner?.change_id || '(none)'}, not ${input.change_id}`,
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
830
910
|
if (input.kind === 'plan') assertPlanWorkflowAuthority(db, { ...input, root_dir: rootDir });
|
|
831
911
|
if (input.kind === 'dev') {
|
|
832
912
|
if (!input.task_id) {
|
|
@@ -873,11 +953,22 @@ function insertWorkflowInTx(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
|
873
953
|
throw new WorkflowStateError('WORKFLOW_IN_PROGRESS', `workflow ${active.id} is already active`);
|
|
874
954
|
}
|
|
875
955
|
|
|
956
|
+
const researchCoverage = input.kind === 'research'
|
|
957
|
+
? normalizeResearchCoverage(mode, input.coverage, input.selected_steps, input.metadata)
|
|
958
|
+
: null;
|
|
959
|
+
const coverageByStep = new Map((researchCoverage || []).map((item) => [item.step_id, item]));
|
|
876
960
|
const selected = new Set(input.kind === 'research'
|
|
877
|
-
?
|
|
961
|
+
? researchCoverage
|
|
962
|
+
.filter((item) => ['execute', 'verify_existing', 'reuse'].includes(item.disposition)
|
|
963
|
+
|| (item.disposition === 'deferred' && !item.accepted_by))
|
|
964
|
+
.map((item) => item.step_id)
|
|
878
965
|
: definition.map((item) => item.id));
|
|
879
966
|
let metadata = input.kind === 'research'
|
|
880
|
-
? {
|
|
967
|
+
? {
|
|
968
|
+
...(input.metadata || {}),
|
|
969
|
+
selected_steps: [...selected],
|
|
970
|
+
coverage: researchCoverage,
|
|
971
|
+
}
|
|
881
972
|
: (input.metadata || {});
|
|
882
973
|
if (input.kind === 'plan') {
|
|
883
974
|
const profile = db.prepare('SELECT kind FROM changes WHERE id = ?').get(input.change_id)?.kind;
|
|
@@ -902,11 +993,31 @@ function insertWorkflowInTx(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
|
902
993
|
);
|
|
903
994
|
definition.forEach((item, position) => {
|
|
904
995
|
const required = selected.has(item.id);
|
|
996
|
+
const coverage = coverageByStep.get(item.id);
|
|
997
|
+
const disposition = coverage?.disposition || null;
|
|
998
|
+
const deferred = disposition === 'deferred' && !coverage.accepted_by;
|
|
999
|
+
const skippedReason = coverage && !required
|
|
1000
|
+
? `${disposition}: ${coverage.rationale}${coverage.accepted_by ? ` (accepted by ${coverage.accepted_by})` : ''}`
|
|
1001
|
+
: (required ? null : `Excluded by ${mode} mode.`);
|
|
905
1002
|
insertStep.run(
|
|
906
1003
|
id, item.id, position, item.title, required ? 1 : 0,
|
|
907
|
-
|
|
1004
|
+
deferred ? 'blocked' : (required ? 'pending' : 'skipped'), skippedReason, ts,
|
|
908
1005
|
);
|
|
909
1006
|
});
|
|
1007
|
+
if (researchCoverage?.some((item) => item.disposition === 'deferred' && !item.accepted_by)) {
|
|
1008
|
+
const blockers = researchCoverage
|
|
1009
|
+
.filter((item) => item.disposition === 'deferred' && !item.accepted_by)
|
|
1010
|
+
.map((item) => `RESEARCH_DEFERRED:${item.step_id}`);
|
|
1011
|
+
db.prepare(
|
|
1012
|
+
"UPDATE workflow_runs SET status = 'blocked', blockers_json = ? WHERE id = ?",
|
|
1013
|
+
).run(JSON.stringify(blockers), id);
|
|
1014
|
+
for (const blocker of blockers) {
|
|
1015
|
+
const stepId = blocker.split(':')[1];
|
|
1016
|
+
db.prepare(
|
|
1017
|
+
'UPDATE workflow_steps SET blockers_json = ? WHERE run_id = ? AND step_id = ?',
|
|
1018
|
+
).run(JSON.stringify([blocker]), id, stepId);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
910
1021
|
ops.appendEventInTx(db, {
|
|
911
1022
|
type: 'workflow_started', task_id: input.task_id, change_id: input.change_id,
|
|
912
1023
|
payload: { workflow_id: id, kind: input.kind, mode, baseline_id: input.baseline_id || null },
|
|
@@ -1008,7 +1119,7 @@ function recordWorkflowStep(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
|
1008
1119
|
throw new WorkflowStateError('WORKFLOW_BLOCKER_REQUIRED', `${stepId} requires a blocker code`);
|
|
1009
1120
|
}
|
|
1010
1121
|
if (input.status === 'completed' && runRow.kind === 'test' && stepId === 'bind-scope') {
|
|
1011
|
-
assertDevelopmentComplete(db, runRow.change_id, rootDir);
|
|
1122
|
+
assertDevelopmentComplete(db, runRow.change_id, rootDir, runRow.task_id);
|
|
1012
1123
|
}
|
|
1013
1124
|
if (input.status === 'completed' && runRow.kind === 'dev' && stepId === 'review-slice') {
|
|
1014
1125
|
assertApprovedReview(db, runRow.change_id, runRow.task_id, rootDir);
|
|
@@ -1016,11 +1127,6 @@ function recordWorkflowStep(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
|
1016
1127
|
if (input.status === 'completed' && runRow.kind === 'deliver' && stepId === 'bind-evidence') {
|
|
1017
1128
|
validateDeliveryPrerequisites(db, { change_id: runRow.change_id }, rootDir);
|
|
1018
1129
|
}
|
|
1019
|
-
if (input.status === 'completed' && runRow.kind === 'deliver'
|
|
1020
|
-
&& stepId === 'release-if-authorized') {
|
|
1021
|
-
validateReleaseAuthorizationDecision(decisions);
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
1130
|
const ts = nowIso();
|
|
1025
1131
|
const startedAt = stepRow.started_at || ts;
|
|
1026
1132
|
const completedAt = input.status === 'completed' ? ts : null;
|
|
@@ -1123,7 +1229,7 @@ function validateContextManifestOutput(db, run, outputs, rootDir, { current = tr
|
|
|
1123
1229
|
let value;
|
|
1124
1230
|
try { value = JSON.parse(fs.readFileSync(resolved.file, 'utf8')); }
|
|
1125
1231
|
catch { continue; }
|
|
1126
|
-
if (value?.schema_version === '
|
|
1232
|
+
if (value?.schema_version === '3.0' && value?.snapshot_id) {
|
|
1127
1233
|
candidate = output;
|
|
1128
1234
|
manifest = value;
|
|
1129
1235
|
break;
|
|
@@ -1131,7 +1237,7 @@ function validateContextManifestOutput(db, run, outputs, rootDir, { current = tr
|
|
|
1131
1237
|
}
|
|
1132
1238
|
if (!candidate) {
|
|
1133
1239
|
throw new WorkflowStateError(
|
|
1134
|
-
'WORKFLOW_CONTEXT_OUTPUT_REQUIRED', `${run.kind} requires a Context Manifest
|
|
1240
|
+
'WORKFLOW_CONTEXT_OUTPUT_REQUIRED', `${run.kind} requires a current Context Manifest output`,
|
|
1135
1241
|
);
|
|
1136
1242
|
}
|
|
1137
1243
|
if (manifest.change?.id !== run.change_id
|
|
@@ -1239,16 +1345,22 @@ function assertCurrentPlan(db, changeId, rootDir) {
|
|
|
1239
1345
|
'WORKFLOW_PLAN_TASK_SET_STALE', `change ${changeId} task set changed after planning`,
|
|
1240
1346
|
);
|
|
1241
1347
|
}
|
|
1242
|
-
const
|
|
1243
|
-
if (!
|
|
1348
|
+
const plannedDigests = plan.summary.task_contract_digests;
|
|
1349
|
+
if (!plannedDigests || typeof plannedDigests !== 'object' || Array.isArray(plannedDigests)) {
|
|
1244
1350
|
throw new WorkflowStateError(
|
|
1245
1351
|
'WORKFLOW_PLAN_TASK_CONTRACT_STALE', `change ${changeId} plan has no task-contract provenance`,
|
|
1246
1352
|
);
|
|
1247
1353
|
}
|
|
1248
1354
|
for (const task of tasks) {
|
|
1249
|
-
if (
|
|
1355
|
+
if (task.stale) {
|
|
1250
1356
|
throw new WorkflowStateError(
|
|
1251
|
-
'WORKFLOW_PLAN_TASK_CONTRACT_STALE',
|
|
1357
|
+
'WORKFLOW_PLAN_TASK_CONTRACT_STALE',
|
|
1358
|
+
`task ${task.id} was invalidated by newer change authority and must be reconciled`,
|
|
1359
|
+
);
|
|
1360
|
+
}
|
|
1361
|
+
if (plannedDigests[task.id] !== taskPlanDigest(task)) {
|
|
1362
|
+
throw new WorkflowStateError(
|
|
1363
|
+
'WORKFLOW_PLAN_TASK_CONTRACT_STALE', `task ${task.id} changed after plan convergence`,
|
|
1252
1364
|
);
|
|
1253
1365
|
}
|
|
1254
1366
|
}
|
|
@@ -1308,12 +1420,23 @@ function assertCompletedDevelopmentEvidence(run, task, rootDir) {
|
|
|
1308
1420
|
return run;
|
|
1309
1421
|
}
|
|
1310
1422
|
|
|
1311
|
-
function assertDevelopmentComplete(db, changeId, rootDir) {
|
|
1312
|
-
const tasks =
|
|
1423
|
+
function assertDevelopmentComplete(db, changeId, rootDir, taskId = null) {
|
|
1424
|
+
const tasks = taskId
|
|
1425
|
+
? [ops.readTask(db, taskId)].filter((task) => task?.change_id === changeId)
|
|
1426
|
+
: changeTasks(db, changeId);
|
|
1313
1427
|
if (tasks.length === 0) {
|
|
1314
|
-
throw new WorkflowStateError(
|
|
1428
|
+
throw new WorkflowStateError(
|
|
1429
|
+
'WORKFLOW_TASKS_REQUIRED',
|
|
1430
|
+
taskId ? `task ${taskId} is not owned by change ${changeId}` : `change ${changeId} has no tasks`,
|
|
1431
|
+
);
|
|
1315
1432
|
}
|
|
1316
1433
|
for (const task of tasks) {
|
|
1434
|
+
if (task.stale) {
|
|
1435
|
+
throw new WorkflowStateError(
|
|
1436
|
+
'WORKFLOW_GATE_STALE',
|
|
1437
|
+
`task ${task.id} was invalidated by newer change authority and must be replanned`,
|
|
1438
|
+
);
|
|
1439
|
+
}
|
|
1317
1440
|
if (!['completed', 'expanded'].includes(task.status)) {
|
|
1318
1441
|
throw new WorkflowStateError(
|
|
1319
1442
|
'WORKFLOW_TASK_NOT_COMPLETED', `task ${task.id} is ${task.status}`,
|
|
@@ -1388,8 +1511,30 @@ function validateDevCompletion(db, run, rootDir) {
|
|
|
1388
1511
|
}
|
|
1389
1512
|
|
|
1390
1513
|
function validateChangeCompletion(db, run, rootDir) {
|
|
1391
|
-
const
|
|
1392
|
-
|
|
1514
|
+
const change = db.prepare(
|
|
1515
|
+
`SELECT id, kind, intent, contract_json, classification_json, research_disposition_json
|
|
1516
|
+
FROM changes WHERE id = ?`,
|
|
1517
|
+
).get(run.change_id);
|
|
1518
|
+
if (!change) {
|
|
1519
|
+
throw new WorkflowStateError(
|
|
1520
|
+
'WORKFLOW_CHANGE_NOT_FOUND', `change ${run.change_id || '(missing)'} does not exist`,
|
|
1521
|
+
);
|
|
1522
|
+
}
|
|
1523
|
+
const contract = parseJson(change.contract_json, 'changes.contract_json', {});
|
|
1524
|
+
const classification = parseJson(change.classification_json, 'changes.classification_json', {});
|
|
1525
|
+
const researchDisposition = parseJson(
|
|
1526
|
+
change.research_disposition_json, 'changes.research_disposition_json', {},
|
|
1527
|
+
);
|
|
1528
|
+
return {
|
|
1529
|
+
change_id: change.id,
|
|
1530
|
+
change_kind: change.kind,
|
|
1531
|
+
acceptance_ids: Array.isArray(contract.acceptance)
|
|
1532
|
+
? contract.acceptance.map((item) => item.id)
|
|
1533
|
+
: [],
|
|
1534
|
+
classification,
|
|
1535
|
+
research_disposition: researchDisposition,
|
|
1536
|
+
authority_basis: 'accepted_change_contract',
|
|
1537
|
+
};
|
|
1393
1538
|
}
|
|
1394
1539
|
|
|
1395
1540
|
function validateDeliveryPrerequisites(db, run, rootDir) {
|
|
@@ -1452,13 +1597,56 @@ function assertCurrentCheckout(report, rootDir) {
|
|
|
1452
1597
|
return checkout;
|
|
1453
1598
|
}
|
|
1454
1599
|
|
|
1600
|
+
const VERIFICATION_DIMENSIONS = Object.freeze([
|
|
1601
|
+
'acceptance',
|
|
1602
|
+
'regression',
|
|
1603
|
+
'integration',
|
|
1604
|
+
'static_analysis',
|
|
1605
|
+
'build',
|
|
1606
|
+
'performance',
|
|
1607
|
+
'security',
|
|
1608
|
+
'recovery',
|
|
1609
|
+
]);
|
|
1610
|
+
|
|
1611
|
+
function validateVerificationProfile(report) {
|
|
1612
|
+
const profile = report.verification_profile;
|
|
1613
|
+
const selected = new Set(profile?.selected_dimensions || []);
|
|
1614
|
+
const dimensions = new Set(Object.keys(report.verification_dimensions || {}));
|
|
1615
|
+
const excludedItems = Array.isArray(profile?.excluded_dimensions)
|
|
1616
|
+
? profile.excluded_dimensions : [];
|
|
1617
|
+
const excluded = new Set(excludedItems.map((item) => item.dimension));
|
|
1618
|
+
const known = new Set(VERIFICATION_DIMENSIONS);
|
|
1619
|
+
const sameSet = (left, right) => left.size === right.size
|
|
1620
|
+
&& [...left].every((item) => right.has(item));
|
|
1621
|
+
const invalid = [...selected, ...dimensions, ...excluded].some((item) => !known.has(item));
|
|
1622
|
+
const overlap = [...selected].some((item) => excluded.has(item));
|
|
1623
|
+
const duplicateExclusion = excluded.size !== excludedItems.length;
|
|
1624
|
+
const completeDisposition = VERIFICATION_DIMENSIONS.every(
|
|
1625
|
+
(dimension) => selected.has(dimension) || excluded.has(dimension),
|
|
1626
|
+
);
|
|
1627
|
+
if (!profile || typeof profile.rationale !== 'string' || profile.rationale.trim().length < 3
|
|
1628
|
+
|| !selected.has('acceptance') || !sameSet(selected, dimensions) || invalid || overlap
|
|
1629
|
+
|| duplicateExclusion || !completeDisposition) {
|
|
1630
|
+
throw new WorkflowStateError(
|
|
1631
|
+
'WORKFLOW_VERIFICATION_PROFILE_INVALID',
|
|
1632
|
+
'verification profile must select exactly the recorded dimensions, include acceptance, and explain every excluded dimension',
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
return {
|
|
1636
|
+
rationale: profile.rationale.trim(),
|
|
1637
|
+
selected_dimensions: [...selected],
|
|
1638
|
+
excluded_dimensions: excludedItems,
|
|
1639
|
+
};
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1455
1642
|
function validateTestCompletion(db, run, rootDir) {
|
|
1456
|
-
assertDevelopmentComplete(db, run.change_id, rootDir);
|
|
1643
|
+
assertDevelopmentComplete(db, run.change_id, rootDir, run.task_id);
|
|
1457
1644
|
const context = recordedContext(db, run, rootDir, { current: true });
|
|
1458
1645
|
const { value: report, output } = readOutputJson(
|
|
1459
1646
|
run, 'write-report', rootDir, 'ultra-test-report-v1',
|
|
1460
1647
|
);
|
|
1461
1648
|
assertReportSchema(report, 'test');
|
|
1649
|
+
const verificationProfile = validateVerificationProfile(report);
|
|
1462
1650
|
if (report.change_id !== run.change_id) {
|
|
1463
1651
|
throw new WorkflowStateError('WORKFLOW_REPORT_CHANGE_MISMATCH', 'test report change_id is not bound to the workflow');
|
|
1464
1652
|
}
|
|
@@ -1505,9 +1693,7 @@ function validateTestCompletion(db, run, rootDir) {
|
|
|
1505
1693
|
&& report.public_seams.every((item) => item?.status === 'pass')
|
|
1506
1694
|
&& report.failures.every((item) => item.status === 'resolved')
|
|
1507
1695
|
&& report.recovery.every((item) => item.status === 'pass')
|
|
1508
|
-
&& Object.values(report.verification_dimensions || {}).every(
|
|
1509
|
-
(item) => ['pass', 'not_applicable'].includes(item?.status),
|
|
1510
|
-
)
|
|
1696
|
+
&& Object.values(report.verification_dimensions || {}).every((item) => item?.status === 'pass')
|
|
1511
1697
|
&& report.blocking_issues.length === 0;
|
|
1512
1698
|
if (typeof report.passed !== 'boolean' || report.passed !== claimedPass) {
|
|
1513
1699
|
throw new WorkflowStateError(
|
|
@@ -1525,12 +1711,21 @@ function validateTestCompletion(db, run, rootDir) {
|
|
|
1525
1711
|
context_path: context.output.path,
|
|
1526
1712
|
context_digest: context.output.digest,
|
|
1527
1713
|
regression_signal: report.regression_signal,
|
|
1714
|
+
verification_profile: verificationProfile,
|
|
1528
1715
|
verification_dimensions: report.verification_dimensions,
|
|
1529
1716
|
blocking_issues: report.blocking_issues,
|
|
1530
1717
|
};
|
|
1531
1718
|
}
|
|
1532
1719
|
|
|
1533
1720
|
const REVIEW_AXES = new Set(['spec_fidelity', 'engineering_standards']);
|
|
1721
|
+
const REVIEW_WORKERS = new Set([
|
|
1722
|
+
'review-spec',
|
|
1723
|
+
'review-code',
|
|
1724
|
+
'review-tests',
|
|
1725
|
+
'review-errors',
|
|
1726
|
+
'review-design',
|
|
1727
|
+
'review-comments',
|
|
1728
|
+
]);
|
|
1534
1729
|
const REVIEW_SEVERITIES = new Set(['P0', 'P1', 'P2', 'P3']);
|
|
1535
1730
|
const REVIEW_FINDING_FIELDS = [
|
|
1536
1731
|
'id', 'axis', 'severity', 'category', 'title', 'file', 'line',
|
|
@@ -1574,6 +1769,7 @@ function readReviewSpecialists(run, rootDir) {
|
|
|
1574
1769
|
const checkout = currentCheckout(rootDir);
|
|
1575
1770
|
const expectedHead = checkout.head || 'workspace';
|
|
1576
1771
|
const seenIds = new Set();
|
|
1772
|
+
const seenAgents = new Set();
|
|
1577
1773
|
const artifacts = [];
|
|
1578
1774
|
for (const [stepId, expectedAxis] of axes) {
|
|
1579
1775
|
const outputs = run.steps.find((item) => item.step_id === stepId)?.outputs || [];
|
|
@@ -1603,6 +1799,13 @@ function readReviewSpecialists(run, rootDir) {
|
|
|
1603
1799
|
'WORKFLOW_REPORT_INVALID', `${output.path} is not a complete ${expectedAxis} artifact`,
|
|
1604
1800
|
);
|
|
1605
1801
|
}
|
|
1802
|
+
if (!REVIEW_WORKERS.has(artifact.agent) || seenAgents.has(artifact.agent)) {
|
|
1803
|
+
throw new WorkflowStateError(
|
|
1804
|
+
'WORKFLOW_REPORT_INVALID',
|
|
1805
|
+
`${output.path} uses an unknown or duplicate review worker: ${artifact.agent}`,
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
seenAgents.add(artifact.agent);
|
|
1606
1809
|
for (const finding of artifact.findings) {
|
|
1607
1810
|
validateReviewFinding(finding, expectedAxis, seenIds, output.path);
|
|
1608
1811
|
}
|
|
@@ -1613,6 +1816,8 @@ function readReviewSpecialists(run, rootDir) {
|
|
|
1613
1816
|
}
|
|
1614
1817
|
|
|
1615
1818
|
function assertCoordinatedReviewEvidence(report, specialists) {
|
|
1819
|
+
const sameSet = (left, right) => left.size === right.size
|
|
1820
|
+
&& [...left].every((item) => right.has(item));
|
|
1616
1821
|
const artifactPathsByAxis = Object.fromEntries([...REVIEW_AXES].map((axis) => [
|
|
1617
1822
|
axis,
|
|
1618
1823
|
specialists.artifacts.filter((artifact) => artifact.axis === axis).map((artifact) => artifact.path).sort(),
|
|
@@ -1633,15 +1838,12 @@ function assertCoordinatedReviewEvidence(report, specialists) {
|
|
|
1633
1838
|
);
|
|
1634
1839
|
}
|
|
1635
1840
|
const completed = new Set(report.workers?.completed || []);
|
|
1636
|
-
|
|
1841
|
+
const artifactAgents = new Set(specialists.artifacts.map((artifact) => artifact.agent));
|
|
1842
|
+
if (!sameSet(completed, artifactAgents)
|
|
1637
1843
|
|| (report.workers?.failed || []).length > 0) {
|
|
1638
1844
|
throw new WorkflowStateError(
|
|
1639
|
-
'WORKFLOW_REVIEW_EVIDENCE_MISMATCH',
|
|
1640
|
-
|
|
1641
|
-
}
|
|
1642
|
-
if ((report.workers?.skipped || []).length > 0 && (report.limitations || []).length === 0) {
|
|
1643
|
-
throw new WorkflowStateError(
|
|
1644
|
-
'WORKFLOW_REVIEW_EVIDENCE_MISMATCH', 'skipped review workers require a recorded limitation',
|
|
1845
|
+
'WORKFLOW_REVIEW_EVIDENCE_MISMATCH',
|
|
1846
|
+
'completed review workers must match specialist artifacts exactly',
|
|
1645
1847
|
);
|
|
1646
1848
|
}
|
|
1647
1849
|
const selection = Array.isArray(report.worker_selection) ? report.worker_selection : [];
|
|
@@ -1664,13 +1866,13 @@ function assertCoordinatedReviewEvidence(report, specialists) {
|
|
|
1664
1866
|
...(report.workers?.completed || []), ...(report.workers?.failed || []),
|
|
1665
1867
|
]);
|
|
1666
1868
|
const expectedSkipped = new Set(report.workers?.skipped || []);
|
|
1667
|
-
|
|
1668
|
-
|
|
1869
|
+
if (!sameSet(seenWorkers, REVIEW_WORKERS)
|
|
1870
|
+
|| !sameSet(selected, expectedSelected)
|
|
1669
1871
|
|| !sameSet(skipped, expectedSkipped)
|
|
1670
1872
|
|| !selected.has('review-spec') || !completed.has('review-spec')) {
|
|
1671
1873
|
throw new WorkflowStateError(
|
|
1672
1874
|
'WORKFLOW_REVIEW_EVIDENCE_MISMATCH',
|
|
1673
|
-
'review worker selection must match completed, failed, and skipped
|
|
1875
|
+
'review worker selection must disposition the complete worker roster and match completed, failed, and skipped state',
|
|
1674
1876
|
);
|
|
1675
1877
|
}
|
|
1676
1878
|
|
|
@@ -1799,6 +2001,12 @@ function validateDeliveryCompletion(db, run, rootDir) {
|
|
|
1799
2001
|
const { value: report, output } = readOutputJson(
|
|
1800
2002
|
run, 'verify-delivery', rootDir, 'ultra-delivery-report-v1',
|
|
1801
2003
|
);
|
|
2004
|
+
if (report.release !== undefined) {
|
|
2005
|
+
throw new WorkflowStateError(
|
|
2006
|
+
'WORKFLOW_RELEASE_AUTHORITY_MISMATCH',
|
|
2007
|
+
'delivery reports cannot create release authority; record external effects through the host after explicit authorization',
|
|
2008
|
+
);
|
|
2009
|
+
}
|
|
1802
2010
|
assertReportSchema(report, 'delivery');
|
|
1803
2011
|
if (report.change_id !== run.change_id || report.archive_status !== 'archived') {
|
|
1804
2012
|
throw new WorkflowStateError(
|
|
@@ -1817,24 +2025,6 @@ function validateDeliveryCompletion(db, run, rootDir) {
|
|
|
1817
2025
|
|| report.checks.some((item) => item?.status !== 'pass')) {
|
|
1818
2026
|
throw new WorkflowStateError('WORKFLOW_DELIVERY_CHECK_FAILED', 'delivery checks must all pass');
|
|
1819
2027
|
}
|
|
1820
|
-
if (!report.release || typeof report.release !== 'object'
|
|
1821
|
-
|| typeof report.release.authorized !== 'boolean'
|
|
1822
|
-
|| typeof report.release.performed !== 'boolean') {
|
|
1823
|
-
throw new WorkflowStateError('WORKFLOW_REPORT_INVALID', 'delivery report release decision is incomplete');
|
|
1824
|
-
}
|
|
1825
|
-
if (report.release.authorized !== report.release.performed) {
|
|
1826
|
-
throw new WorkflowStateError(
|
|
1827
|
-
'WORKFLOW_RELEASE_INCOMPLETE', 'an authorized release must complete; an unauthorized release must not run',
|
|
1828
|
-
);
|
|
1829
|
-
}
|
|
1830
|
-
const releaseStep = run.steps.find((item) => item.step_id === 'release-if-authorized');
|
|
1831
|
-
const releaseDecision = validateReleaseAuthorizationDecision(releaseStep?.decisions || []);
|
|
1832
|
-
if (report.release.authorized !== releaseDecision.authorized) {
|
|
1833
|
-
throw new WorkflowStateError(
|
|
1834
|
-
'WORKFLOW_RELEASE_AUTHORITY_MISMATCH',
|
|
1835
|
-
'delivery report release claim does not match the recorded owner authorization decision',
|
|
1836
|
-
);
|
|
1837
|
-
}
|
|
1838
2028
|
if (!String(report.rollback || '').trim()) {
|
|
1839
2029
|
throw new WorkflowStateError('WORKFLOW_REPORT_INVALID', 'delivery report rollback guidance is required');
|
|
1840
2030
|
}
|
|
@@ -1850,7 +2040,6 @@ function validateDeliveryCompletion(db, run, rootDir) {
|
|
|
1850
2040
|
baseline_status: baseline.status,
|
|
1851
2041
|
git_commit: checkout.head,
|
|
1852
2042
|
worktree_digest: checkout.digest,
|
|
1853
|
-
release: report.release,
|
|
1854
2043
|
report_path: output.path,
|
|
1855
2044
|
report_digest: output.digest,
|
|
1856
2045
|
context_path: context.output.path,
|
|
@@ -1910,24 +2099,6 @@ function validatePlanArtifact(db, run, rootDir) {
|
|
|
1910
2099
|
};
|
|
1911
2100
|
}
|
|
1912
2101
|
|
|
1913
|
-
function advanceLinkedChangeAfterPlan(db, run, rootDir) {
|
|
1914
|
-
if (run.kind !== 'plan' || !run.change_id) return;
|
|
1915
|
-
const linked = db.prepare(
|
|
1916
|
-
`SELECT id FROM workflow_runs
|
|
1917
|
-
WHERE kind = 'change' AND change_id = ? AND current_step = 'plan-change'
|
|
1918
|
-
AND status IN ('active', 'blocked')
|
|
1919
|
-
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
1920
|
-
).get(run.change_id);
|
|
1921
|
-
if (!linked) return;
|
|
1922
|
-
recordWorkflowStep(db, {
|
|
1923
|
-
id: linked.id, step_id: 'plan-change', status: 'completed',
|
|
1924
|
-
evidence: [{
|
|
1925
|
-
kind: 'workflow', ref: run.id,
|
|
1926
|
-
summary: `Plan workflow ${run.id} completed with authoritative task contracts.`,
|
|
1927
|
-
}],
|
|
1928
|
-
}, { rootDir });
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
2102
|
function completeWorkflow(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
1932
2103
|
const id = nonEmpty(input.id, 'id');
|
|
1933
2104
|
return ops.tx(db, () => {
|
|
@@ -1974,8 +2145,9 @@ function completeWorkflow(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
|
1974
2145
|
task_contract_digests: contract.task_contract_digests,
|
|
1975
2146
|
profile: contract.profile,
|
|
1976
2147
|
coverage: contract.coverage,
|
|
2148
|
+
authority_basis: 'accepted_change_contract',
|
|
1977
2149
|
};
|
|
1978
|
-
validatePlanApproval(approval);
|
|
2150
|
+
if (approval !== null) validatePlanApproval(approval);
|
|
1979
2151
|
}
|
|
1980
2152
|
if (run.kind === 'dev' && ops.readTask(db, run.task_id)?.status !== 'completed') {
|
|
1981
2153
|
throw new WorkflowStateError(
|
|
@@ -1992,7 +2164,7 @@ function completeWorkflow(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
|
1992
2164
|
type: 'workflow_completed', task_id: run.task_id, change_id: run.change_id,
|
|
1993
2165
|
payload: { workflow_id: id, kind: run.kind, baseline_id: run.baseline_id },
|
|
1994
2166
|
});
|
|
1995
|
-
if (run.kind === 'plan') {
|
|
2167
|
+
if (run.kind === 'plan' && approval !== null) {
|
|
1996
2168
|
ops.appendEventInTx(db, {
|
|
1997
2169
|
type: 'plan_approved', change_id: run.change_id,
|
|
1998
2170
|
payload: {
|
|
@@ -2005,7 +2177,6 @@ function completeWorkflow(db, input = {}, { rootDir = process.cwd() } = {}) {
|
|
|
2005
2177
|
},
|
|
2006
2178
|
});
|
|
2007
2179
|
}
|
|
2008
|
-
advanceLinkedChangeAfterPlan(db, run, rootDir);
|
|
2009
2180
|
return readWorkflow(db, id, { rootDir });
|
|
2010
2181
|
});
|
|
2011
2182
|
}
|
|
@@ -2064,6 +2235,12 @@ function validatePlanContract(db, run, rootDir = process.cwd()) {
|
|
|
2064
2235
|
);
|
|
2065
2236
|
}
|
|
2066
2237
|
ops.assertTaskExecutionContract(task);
|
|
2238
|
+
if (task.stale) {
|
|
2239
|
+
throw new WorkflowStateError(
|
|
2240
|
+
'WORKFLOW_PLAN_TASK_CONTRACT_STALE',
|
|
2241
|
+
`task ${id} was invalidated by newer change authority and must be reconciled`,
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2067
2244
|
return task;
|
|
2068
2245
|
});
|
|
2069
2246
|
const changeRow = db.prepare(
|
|
@@ -2128,88 +2305,6 @@ function validatePlanContract(db, run, rootDir = process.cwd()) {
|
|
|
2128
2305
|
};
|
|
2129
2306
|
}
|
|
2130
2307
|
|
|
2131
|
-
function activeInitializationRun(db, baselineId) {
|
|
2132
|
-
return db.prepare(
|
|
2133
|
-
`SELECT * FROM workflow_runs
|
|
2134
|
-
WHERE kind = 'init' AND baseline_id = ? AND status IN ('active', 'blocked', 'ready')
|
|
2135
|
-
ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
|
|
2136
|
-
).get(baselineId);
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
function blockInitializationInTx(db, baselineId, blockers = []) {
|
|
2140
|
-
const run = activeInitializationRun(db, baselineId);
|
|
2141
|
-
if (!run) return null;
|
|
2142
|
-
const normalized = [...new Set(blockers.map((item) => String(item).trim()).filter(Boolean))];
|
|
2143
|
-
const ts = nowIso();
|
|
2144
|
-
db.prepare(
|
|
2145
|
-
`UPDATE workflow_steps SET status = 'blocked', blockers_json = ?,
|
|
2146
|
-
started_at = COALESCE(started_at, ?), completed_at = NULL, updated_at = ?
|
|
2147
|
-
WHERE run_id = ? AND step_id = 'establish-baseline'`,
|
|
2148
|
-
).run(JSON.stringify(normalized), ts, ts, run.id);
|
|
2149
|
-
db.prepare(
|
|
2150
|
-
`UPDATE workflow_runs SET status = 'blocked', current_step = 'establish-baseline',
|
|
2151
|
-
blockers_json = ?, updated_at = ? WHERE id = ?`,
|
|
2152
|
-
).run(JSON.stringify(normalized), ts, run.id);
|
|
2153
|
-
ops.appendEventInTx(db, {
|
|
2154
|
-
type: 'workflow_blocked',
|
|
2155
|
-
payload: {
|
|
2156
|
-
workflow_id: run.id, kind: 'init', step_id: 'establish-baseline',
|
|
2157
|
-
status: 'blocked', blockers: normalized,
|
|
2158
|
-
},
|
|
2159
|
-
});
|
|
2160
|
-
return run.id;
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
function completeInitializationInTx(db, baseline) {
|
|
2164
|
-
const run = activeInitializationRun(db, baseline.id);
|
|
2165
|
-
if (!run) return null;
|
|
2166
|
-
const ts = nowIso();
|
|
2167
|
-
const steps = [
|
|
2168
|
-
{
|
|
2169
|
-
id: 'establish-baseline',
|
|
2170
|
-
evidence: [{
|
|
2171
|
-
kind: 'baseline', ref: baseline.id,
|
|
2172
|
-
summary: `Baseline ${baseline.id} converged at revision ${baseline.repository_revision}.`,
|
|
2173
|
-
}],
|
|
2174
|
-
},
|
|
2175
|
-
{
|
|
2176
|
-
id: 'verify-initialization',
|
|
2177
|
-
evidence: [{
|
|
2178
|
-
kind: 'state', ref: '.ultra/state.db',
|
|
2179
|
-
summary: 'Baseline authority, workflow state, and the post-initialization route are ready.',
|
|
2180
|
-
}],
|
|
2181
|
-
},
|
|
2182
|
-
];
|
|
2183
|
-
for (const item of steps) {
|
|
2184
|
-
db.prepare(
|
|
2185
|
-
`UPDATE workflow_steps SET status = 'completed', evidence_json = ?, blockers_json = '[]',
|
|
2186
|
-
started_at = COALESCE(started_at, ?), completed_at = ?, updated_at = ?
|
|
2187
|
-
WHERE run_id = ? AND step_id = ?`,
|
|
2188
|
-
).run(JSON.stringify(item.evidence), ts, ts, ts, run.id, item.id);
|
|
2189
|
-
ops.appendEventInTx(db, {
|
|
2190
|
-
type: 'workflow_step_updated',
|
|
2191
|
-
payload: { workflow_id: run.id, kind: 'init', step_id: item.id, status: 'completed' },
|
|
2192
|
-
});
|
|
2193
|
-
}
|
|
2194
|
-
db.prepare(
|
|
2195
|
-
`UPDATE workflow_runs SET status = 'completed', current_step = NULL, blockers_json = '[]',
|
|
2196
|
-
approval_json = ?, summary_json = ?, completed_at = ?, updated_at = ? WHERE id = ?`,
|
|
2197
|
-
).run(
|
|
2198
|
-
JSON.stringify({ approved_by: baseline.approved_by, approved_at: baseline.converged_at }),
|
|
2199
|
-
JSON.stringify({
|
|
2200
|
-
baseline_id: baseline.id,
|
|
2201
|
-
repository_revision: baseline.repository_revision,
|
|
2202
|
-
research_run_id: baseline.research_run_id,
|
|
2203
|
-
}),
|
|
2204
|
-
ts, ts, run.id,
|
|
2205
|
-
);
|
|
2206
|
-
ops.appendEventInTx(db, {
|
|
2207
|
-
type: 'workflow_completed',
|
|
2208
|
-
payload: { workflow_id: run.id, kind: 'init', baseline_id: baseline.id },
|
|
2209
|
-
});
|
|
2210
|
-
return run.id;
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
2308
|
function inspectWorkflowHealth(db, { rootDir = process.cwd() } = {}) {
|
|
2214
2309
|
const all = listWorkflows(db, { limit: 500 }, { rootDir });
|
|
2215
2310
|
const active = all
|
|
@@ -2295,11 +2390,11 @@ module.exports = {
|
|
|
2295
2390
|
listWorkflows,
|
|
2296
2391
|
recordWorkflowStep,
|
|
2297
2392
|
completeWorkflow,
|
|
2298
|
-
blockInitializationInTx,
|
|
2299
|
-
completeInitializationInTx,
|
|
2300
2393
|
inspectWorkflowHealth,
|
|
2301
2394
|
recoverUntrackedChangeWorkflows,
|
|
2302
2395
|
validateDeliveryPrerequisites,
|
|
2303
2396
|
validatePlanContract,
|
|
2397
|
+
assertCurrentPlan,
|
|
2398
|
+
assertApprovedReview,
|
|
2304
2399
|
resolveProjectSourceRef: sourceRefFile,
|
|
2305
2400
|
};
|