ultra-builder-pro-cli 0.15.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.
Files changed (71) hide show
  1. package/.ultra-template/specs/research-distillate.md +4 -3
  2. package/CHANGELOG.md +120 -1
  3. package/README.md +67 -30
  4. package/adapters/_shared/codex-assets.cjs +10 -5
  5. package/adapters/_shared/handbook.cjs +3 -2
  6. package/adapters/_shared/interaction-contract.cjs +61 -0
  7. package/adapters/claude.js +1 -1
  8. package/adapters/opencode.js +1 -1
  9. package/bin/orchestrator.js +175 -19
  10. package/commands/ultra-change.md +1 -1
  11. package/commands/ultra-deliver.md +3 -3
  12. package/commands/ultra-init.md +1 -1
  13. package/commands/ultra-plan.md +1 -1
  14. package/commands/ultra-research.md +2 -2
  15. package/commands/ultra-review.md +10 -0
  16. package/commands/ultra-status.md +1 -1
  17. package/commands/ultra-test.md +1 -1
  18. package/commands/ultra-think.md +2 -2
  19. package/docs/AGENT-CONTEXT.md +38 -18
  20. package/docs/ARCHITECTURE.md +91 -13
  21. package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
  22. package/docs/PLAN.zh-CN.md +19 -12
  23. package/docs/ROADMAP.md +5 -3
  24. package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
  25. package/docs/STATE-DB-ACCESS-POLICY.md +8 -6
  26. package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
  27. package/docs/WORKFLOW-LIFECYCLE.md +179 -39
  28. package/hooks/pre_stop_check.py +3 -2
  29. package/mcp-server/lib/baseline-workflow.cjs +84 -16
  30. package/mcp-server/lib/change-workflow.cjs +107 -39
  31. package/mcp-server/lib/context-spine.cjs +215 -87
  32. package/mcp-server/lib/decision-dialogue.cjs +713 -0
  33. package/mcp-server/lib/doctor.cjs +9 -0
  34. package/mcp-server/lib/git-bootstrap.cjs +240 -0
  35. package/mcp-server/lib/init-project.cjs +178 -41
  36. package/mcp-server/lib/project-breadcrumb.cjs +23 -14
  37. package/mcp-server/lib/state-db.cjs +279 -9
  38. package/mcp-server/lib/state-ops.cjs +64 -1
  39. package/mcp-server/lib/workflow-state.cjs +313 -205
  40. package/mcp-server/server.cjs +112 -34
  41. package/orchestrator/auto-merge.cjs +19 -0
  42. package/orchestrator/daemon.cjs +104 -0
  43. package/orchestrator/parallel-orchestrator.cjs +371 -49
  44. package/orchestrator/session-runner.cjs +354 -25
  45. package/package.json +2 -1
  46. package/skills/ultra-change/SKILL.md +69 -65
  47. package/skills/ultra-deliver/SKILL.md +37 -58
  48. package/skills/ultra-dev/SKILL.md +55 -49
  49. package/skills/ultra-doctor/SKILL.md +44 -49
  50. package/skills/ultra-init/SKILL.md +78 -100
  51. package/skills/ultra-plan/SKILL.md +78 -79
  52. package/skills/ultra-research/SKILL.md +99 -98
  53. package/skills/ultra-review/SKILL.md +38 -47
  54. package/skills/ultra-review/references/review-modes.md +5 -3
  55. package/skills/ultra-review/references/unified-schema.md +26 -6
  56. package/skills/ultra-review/scripts/review_wait.py +12 -0
  57. package/skills/ultra-status/SKILL.md +34 -40
  58. package/skills/ultra-test/SKILL.md +61 -48
  59. package/skills/ultra-think/SKILL.md +47 -47
  60. package/skills/ultra-think/references/decision-dialogue.md +106 -0
  61. package/spec/cli-protocol.md +46 -2
  62. package/spec/mcp-tools.yaml +444 -29
  63. package/spec/schemas/delivery-report.v1.schema.json +1 -46
  64. package/spec/schemas/mcp-tools.schema.json +1 -1
  65. package/spec/schemas/state-db.sql +66 -2
  66. package/spec/schemas/test-report.v1.schema.json +45 -14
  67. package/ultra-tools/cli.cjs +1 -1
  68. package/ultra-tools/commands/session.cjs +10 -3
  69. package/ultra-tools/commands/status.cjs +34 -14
  70. package/ultra-tools/commands/system.cjs +2 -2
  71. package/ultra-tools/commands/task.cjs +3 -0
@@ -8,10 +8,11 @@ const Ajv = require('ajv/dist/2020');
8
8
  const addFormats = require('ajv-formats');
9
9
 
10
10
  const ops = require('./state-ops.cjs');
11
+ const decisionDialogue = require('./decision-dialogue.cjs');
11
12
  const testReportSchema = require('../../spec/schemas/test-report.v1.schema.json');
12
13
  const deliveryReportSchema = require('../../spec/schemas/delivery-report.v1.schema.json');
13
14
 
14
- const DEFINITION_VERSION = '1.1';
15
+ const DEFINITION_VERSION = '2.0';
15
16
  const RUN_KINDS = new Set(['init', 'research', 'plan', 'change', 'dev', 'test', 'review', 'deliver']);
16
17
  const RUN_STATUSES = new Set(['active', 'blocked', 'ready', 'completed', 'cancelled']);
17
18
  const STEP_STATUSES = new Set(['pending', 'in_progress', 'completed', 'skipped', 'blocked']);
@@ -32,8 +33,7 @@ const WORKFLOW_DEFINITIONS = Object.freeze({
32
33
  step('inspect-authority', 'Inspect repository and existing Ultra authority'),
33
34
  step('classify-repository', 'Classify greenfield, brownfield, or migrated state'),
34
35
  step('scaffold-authority', 'Create or resume the authoritative state and scaffold'),
35
- step('establish-baseline', 'Record evidence and establish the project baseline', { evidence_required: true }),
36
- 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 }),
37
37
  ]),
38
38
  research: Object.freeze([
39
39
  step('00-problem-validation', 'Validate the problem and demand evidence', { evidence_required: true, output_required: true }),
@@ -56,12 +56,10 @@ const WORKFLOW_DEFINITIONS = Object.freeze({
56
56
  ]),
57
57
  plan: Object.freeze([
58
58
  step('validate-baseline', 'Validate baseline and research coverage', { evidence_required: true }),
59
- step('select-posture', 'Record the owner-selected planning posture', { evidence_required: true }),
60
59
  step('analyze-requirements', 'Trace requirements and acceptance'),
61
60
  step('analyze-codebase', 'Inspect current codebase patterns and boundaries', { evidence_required: true }),
62
61
  step('design-slices', 'Design walking skeleton and vertical slices'),
63
62
  step('validate-dependencies', 'Validate ownership, dependencies, and parallel waves', { evidence_required: true }),
64
- step('approve-plan', 'Obtain explicit plan approval', { evidence_required: true }),
65
63
  step('persist-task-contracts', 'Persist tasks and complete execution contracts', { evidence_required: true }),
66
64
  step('verify-plan', 'Read back and verify the authoritative plan', {
67
65
  evidence_required: true, output_required: true,
@@ -71,9 +69,6 @@ const WORKFLOW_DEFINITIONS = Object.freeze({
71
69
  step('bind-baseline', 'Bind current baseline and repository revision', { evidence_required: true }),
72
70
  step('classify-change', 'Classify quick, standard, major, or incident'),
73
71
  step('record-intent', 'Record intent, scope, acceptance, and documentation impact', { evidence_required: true }),
74
- step('plan-change', 'Create or validate change tasks'),
75
- step('compile-context', 'Compile bounded role context', { evidence_required: true, output_required: true }),
76
- step('verify-readiness', 'Verify one executable next action', { evidence_required: true }),
77
72
  ]),
78
73
  dev: Object.freeze([
79
74
  step('bind-task', 'Bind the authoritative task and dependencies', { evidence_required: true }),
@@ -88,10 +83,10 @@ const WORKFLOW_DEFINITIONS = Object.freeze({
88
83
  step('bind-scope', 'Bind completed tasks and current revision', { evidence_required: true }),
89
84
  step('compile-context', 'Compile independent checking context', { evidence_required: true, output_required: true }),
90
85
  step('map-acceptance', 'Map acceptance claims to executable checks'),
91
- step('execute-checks', 'Execute focused, regression, static, build, and recovery checks', { evidence_required: true }),
86
+ step('execute-checks', 'Execute the risk-selected verification profile', { evidence_required: true }),
92
87
  step('verify-public-seam', 'Verify the declared public seam', { evidence_required: true }),
93
88
  step('write-report', 'Write the current test evidence report', { evidence_required: true, output_required: true }),
94
- step('verify-test-gate', 'Verify report freshness and route the next action', { evidence_required: true }),
89
+ step('verify-test-gate', 'Verify report freshness and expose valid transitions', { evidence_required: true }),
95
90
  ]),
96
91
  review: Object.freeze([
97
92
  step('bind-diff', 'Bind one current diff and acceptance scope', { evidence_required: true }),
@@ -104,14 +99,17 @@ const WORKFLOW_DEFINITIONS = Object.freeze({
104
99
  deliver: Object.freeze([
105
100
  step('bind-evidence', 'Bind current test, review, task, and revision evidence', { evidence_required: true }),
106
101
  step('reconcile-specifications', 'Resolve learning and reconcile specifications', { evidence_required: true }),
107
- step('verify-candidate', 'Verify the release candidate and recovery path', { evidence_required: true, output_required: true }),
102
+ step('verify-candidate', 'Verify the local delivery candidate and recovery path', { evidence_required: true, output_required: true }),
108
103
  step('converge-authority', 'Converge baseline or change authority', { evidence_required: true }),
109
104
  step('archive-change', 'Archive the converged change when applicable', { evidence_required: true }),
110
- step('release-if-authorized', 'Release only when explicitly authorized'),
111
- 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 }),
112
106
  ]),
113
107
  });
114
108
 
109
+ const RESEARCH_DISPOSITIONS = new Set([
110
+ 'execute', 'verify_existing', 'reuse', 'not_applicable', 'deferred',
111
+ ]);
112
+
115
113
  const RESEARCH_MODES = Object.freeze({
116
114
  full: WORKFLOW_DEFINITIONS.research.map((item) => item.id),
117
115
  adoption: WORKFLOW_DEFINITIONS.research.map((item) => item.id),
@@ -379,32 +377,6 @@ function validateSynthesisTrace(db, runRow, semanticRecords) {
379
377
  }
380
378
  }
381
379
 
382
- function validateReleaseAuthorizationDecision(decisions) {
383
- if (!Array.isArray(decisions) || decisions.length !== 1) {
384
- throw new WorkflowStateError(
385
- 'WORKFLOW_RELEASE_AUTHORIZATION_INVALID',
386
- 'release-if-authorized requires exactly one release_authorization decision',
387
- );
388
- }
389
- const decision = decisions[0];
390
- if (decision?.kind !== 'release_authorization'
391
- || typeof decision.authorized !== 'boolean'
392
- || !String(decision.reason || '').trim()) {
393
- throw new WorkflowStateError(
394
- 'WORKFLOW_RELEASE_AUTHORIZATION_INVALID',
395
- 'release authorization requires kind, authorized, and reason',
396
- );
397
- }
398
- if (decision.authorized === true
399
- && (!String(decision.approved_by || '').trim() || !String(decision.scope || '').trim())) {
400
- throw new WorkflowStateError(
401
- 'WORKFLOW_RELEASE_AUTHORIZATION_INVALID',
402
- 'an authorized release requires approved_by and an explicit release scope',
403
- );
404
- }
405
- return decision;
406
- }
407
-
408
380
  function validateResearchStepReport(run, stepId, outputs, rootDir) {
409
381
  const expected = path.join(
410
382
  '.ultra', 'docs', 'research', run.id, `${stepId}.md`,
@@ -601,7 +573,95 @@ function listWorkflows(db, filter = {}, { rootDir = process.cwd() } = {}) {
601
573
  return rows.map((row) => decorateRun(db, row, rootDir));
602
574
  }
603
575
 
604
- function selectedResearchSteps(mode, selectedSteps) {
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
+ }
605
665
  if (mode === 'custom') {
606
666
  if (!Array.isArray(selectedSteps) || selectedSteps.length === 0) {
607
667
  throw new WorkflowStateError('VALIDATION_ERROR', 'custom research requires selected_steps');
@@ -609,11 +669,21 @@ function selectedResearchSteps(mode, selectedSteps) {
609
669
  const known = new Set(WORKFLOW_DEFINITIONS.research.map((item) => item.id));
610
670
  const invalid = selectedSteps.find((item) => !known.has(item));
611
671
  if (invalid) throw new WorkflowStateError('VALIDATION_ERROR', `unknown research step: ${invalid}`);
612
- 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
+ }));
613
678
  }
614
679
  const selected = RESEARCH_MODES[mode];
615
680
  if (!selected) throw new WorkflowStateError('VALIDATION_ERROR', `unsupported research mode: ${mode}`);
616
- 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
+ }));
617
687
  }
618
688
 
619
689
  function assertReference(db, table, id, field) {
@@ -631,9 +701,10 @@ function assertWorkflowAuthority(db, input, mode) {
631
701
  );
632
702
  }
633
703
  };
634
- if (input.kind !== 'research' && (input.mode !== undefined || input.selected_steps !== undefined)) {
704
+ if (input.kind !== 'research'
705
+ && (input.mode !== undefined || input.selected_steps !== undefined || input.coverage !== undefined)) {
635
706
  throw new WorkflowStateError(
636
- 'VALIDATION_ERROR', 'mode and selected_steps are valid only for research workflows',
707
+ 'VALIDATION_ERROR', 'mode, selected_steps, and coverage are valid only for research workflows',
637
708
  );
638
709
  }
639
710
  if (input.kind === 'init') requireField('baseline_id');
@@ -731,9 +802,10 @@ function assertPlanWorkflowAuthority(db, input) {
731
802
  ORDER BY completed_at DESC, rowid DESC LIMIT 1`,
732
803
  ).get(change.id, disposition.mode);
733
804
  const research = row ? readWorkflow(db, row.id, { rootDir: input.root_dir || process.cwd() }) : null;
734
- const expectedSteps = new Set(selectedResearchSteps(
735
- disposition.mode, disposition.selected_steps,
736
- ));
805
+ const expectedSteps = new Set([
806
+ ...(Array.isArray(disposition.selected_steps) ? disposition.selected_steps : []),
807
+ '99-synthesis',
808
+ ]);
737
809
  const actualSteps = new Set(
738
810
  (research?.steps || []).filter((item) => item.required).map((item) => item.step_id),
739
811
  );
@@ -826,6 +898,15 @@ function insertWorkflowInTx(db, input = {}, { rootDir = process.cwd() } = {}) {
826
898
  assertReference(db, 'baselines', input.baseline_id, 'baseline_id');
827
899
  assertReference(db, 'changes', input.change_id, 'change_id');
828
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
+ }
829
910
  if (input.kind === 'plan') assertPlanWorkflowAuthority(db, { ...input, root_dir: rootDir });
830
911
  if (input.kind === 'dev') {
831
912
  if (!input.task_id) {
@@ -872,11 +953,22 @@ function insertWorkflowInTx(db, input = {}, { rootDir = process.cwd() } = {}) {
872
953
  throw new WorkflowStateError('WORKFLOW_IN_PROGRESS', `workflow ${active.id} is already active`);
873
954
  }
874
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]));
875
960
  const selected = new Set(input.kind === 'research'
876
- ? selectedResearchSteps(mode, input.selected_steps)
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)
877
965
  : definition.map((item) => item.id));
878
966
  let metadata = input.kind === 'research'
879
- ? { ...(input.metadata || {}), selected_steps: [...selected] }
967
+ ? {
968
+ ...(input.metadata || {}),
969
+ selected_steps: [...selected],
970
+ coverage: researchCoverage,
971
+ }
880
972
  : (input.metadata || {});
881
973
  if (input.kind === 'plan') {
882
974
  const profile = db.prepare('SELECT kind FROM changes WHERE id = ?').get(input.change_id)?.kind;
@@ -901,11 +993,31 @@ function insertWorkflowInTx(db, input = {}, { rootDir = process.cwd() } = {}) {
901
993
  );
902
994
  definition.forEach((item, position) => {
903
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.`);
904
1002
  insertStep.run(
905
1003
  id, item.id, position, item.title, required ? 1 : 0,
906
- required ? 'pending' : 'skipped', required ? null : `Excluded by ${mode} mode.`, ts,
1004
+ deferred ? 'blocked' : (required ? 'pending' : 'skipped'), skippedReason, ts,
907
1005
  );
908
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
+ }
909
1021
  ops.appendEventInTx(db, {
910
1022
  type: 'workflow_started', task_id: input.task_id, change_id: input.change_id,
911
1023
  payload: { workflow_id: id, kind: input.kind, mode, baseline_id: input.baseline_id || null },
@@ -947,6 +1059,13 @@ function recordWorkflowStep(db, input = {}, { rootDir = process.cwd() } = {}) {
947
1059
  `cannot transition ${id}/${stepId} from ${stepRow.status} to ${input.status}`,
948
1060
  );
949
1061
  }
1062
+ if (input.status === 'completed') {
1063
+ decisionDialogue.assertDecisionGate(db, {
1064
+ workflow_run_id: runRow.id,
1065
+ change_id: runRow.change_id,
1066
+ baseline_id: runRow.baseline_id,
1067
+ }, { rootDir });
1068
+ }
950
1069
  const firstOpen = db.prepare(
951
1070
  `SELECT step_id FROM workflow_steps
952
1071
  WHERE run_id = ? AND required = 1 AND status NOT IN ('completed', 'skipped')
@@ -1000,7 +1119,7 @@ function recordWorkflowStep(db, input = {}, { rootDir = process.cwd() } = {}) {
1000
1119
  throw new WorkflowStateError('WORKFLOW_BLOCKER_REQUIRED', `${stepId} requires a blocker code`);
1001
1120
  }
1002
1121
  if (input.status === 'completed' && runRow.kind === 'test' && stepId === 'bind-scope') {
1003
- assertDevelopmentComplete(db, runRow.change_id, rootDir);
1122
+ assertDevelopmentComplete(db, runRow.change_id, rootDir, runRow.task_id);
1004
1123
  }
1005
1124
  if (input.status === 'completed' && runRow.kind === 'dev' && stepId === 'review-slice') {
1006
1125
  assertApprovedReview(db, runRow.change_id, runRow.task_id, rootDir);
@@ -1008,11 +1127,6 @@ function recordWorkflowStep(db, input = {}, { rootDir = process.cwd() } = {}) {
1008
1127
  if (input.status === 'completed' && runRow.kind === 'deliver' && stepId === 'bind-evidence') {
1009
1128
  validateDeliveryPrerequisites(db, { change_id: runRow.change_id }, rootDir);
1010
1129
  }
1011
- if (input.status === 'completed' && runRow.kind === 'deliver'
1012
- && stepId === 'release-if-authorized') {
1013
- validateReleaseAuthorizationDecision(decisions);
1014
- }
1015
-
1016
1130
  const ts = nowIso();
1017
1131
  const startedAt = stepRow.started_at || ts;
1018
1132
  const completedAt = input.status === 'completed' ? ts : null;
@@ -1115,7 +1229,7 @@ function validateContextManifestOutput(db, run, outputs, rootDir, { current = tr
1115
1229
  let value;
1116
1230
  try { value = JSON.parse(fs.readFileSync(resolved.file, 'utf8')); }
1117
1231
  catch { continue; }
1118
- if (value?.schema_version === '2.0' && value?.snapshot_id) {
1232
+ if (value?.schema_version === '3.0' && value?.snapshot_id) {
1119
1233
  candidate = output;
1120
1234
  manifest = value;
1121
1235
  break;
@@ -1123,7 +1237,7 @@ function validateContextManifestOutput(db, run, outputs, rootDir, { current = tr
1123
1237
  }
1124
1238
  if (!candidate) {
1125
1239
  throw new WorkflowStateError(
1126
- 'WORKFLOW_CONTEXT_OUTPUT_REQUIRED', `${run.kind} requires a Context Manifest v2 output`,
1240
+ 'WORKFLOW_CONTEXT_OUTPUT_REQUIRED', `${run.kind} requires a current Context Manifest output`,
1127
1241
  );
1128
1242
  }
1129
1243
  if (manifest.change?.id !== run.change_id
@@ -1231,16 +1345,22 @@ function assertCurrentPlan(db, changeId, rootDir) {
1231
1345
  'WORKFLOW_PLAN_TASK_SET_STALE', `change ${changeId} task set changed after planning`,
1232
1346
  );
1233
1347
  }
1234
- const approvedDigests = plan.summary.task_contract_digests;
1235
- if (!approvedDigests || typeof approvedDigests !== 'object' || Array.isArray(approvedDigests)) {
1348
+ const plannedDigests = plan.summary.task_contract_digests;
1349
+ if (!plannedDigests || typeof plannedDigests !== 'object' || Array.isArray(plannedDigests)) {
1236
1350
  throw new WorkflowStateError(
1237
1351
  'WORKFLOW_PLAN_TASK_CONTRACT_STALE', `change ${changeId} plan has no task-contract provenance`,
1238
1352
  );
1239
1353
  }
1240
1354
  for (const task of tasks) {
1241
- if (approvedDigests[task.id] !== taskPlanDigest(task)) {
1355
+ if (task.stale) {
1242
1356
  throw new WorkflowStateError(
1243
- 'WORKFLOW_PLAN_TASK_CONTRACT_STALE', `task ${task.id} changed after plan approval`,
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`,
1244
1364
  );
1245
1365
  }
1246
1366
  }
@@ -1300,12 +1420,23 @@ function assertCompletedDevelopmentEvidence(run, task, rootDir) {
1300
1420
  return run;
1301
1421
  }
1302
1422
 
1303
- function assertDevelopmentComplete(db, changeId, rootDir) {
1304
- const tasks = changeTasks(db, changeId);
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);
1305
1427
  if (tasks.length === 0) {
1306
- throw new WorkflowStateError('WORKFLOW_TASKS_REQUIRED', `change ${changeId} has no tasks`);
1428
+ throw new WorkflowStateError(
1429
+ 'WORKFLOW_TASKS_REQUIRED',
1430
+ taskId ? `task ${taskId} is not owned by change ${changeId}` : `change ${changeId} has no tasks`,
1431
+ );
1307
1432
  }
1308
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
+ }
1309
1440
  if (!['completed', 'expanded'].includes(task.status)) {
1310
1441
  throw new WorkflowStateError(
1311
1442
  'WORKFLOW_TASK_NOT_COMPLETED', `task ${task.id} is ${task.status}`,
@@ -1380,8 +1511,30 @@ function validateDevCompletion(db, run, rootDir) {
1380
1511
  }
1381
1512
 
1382
1513
  function validateChangeCompletion(db, run, rootDir) {
1383
- const { plan, taskIds } = assertCurrentPlan(db, run.change_id, rootDir);
1384
- return { plan_workflow_id: plan.id, task_ids: taskIds };
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
+ };
1385
1538
  }
1386
1539
 
1387
1540
  function validateDeliveryPrerequisites(db, run, rootDir) {
@@ -1444,13 +1597,56 @@ function assertCurrentCheckout(report, rootDir) {
1444
1597
  return checkout;
1445
1598
  }
1446
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
+
1447
1642
  function validateTestCompletion(db, run, rootDir) {
1448
- assertDevelopmentComplete(db, run.change_id, rootDir);
1643
+ assertDevelopmentComplete(db, run.change_id, rootDir, run.task_id);
1449
1644
  const context = recordedContext(db, run, rootDir, { current: true });
1450
1645
  const { value: report, output } = readOutputJson(
1451
1646
  run, 'write-report', rootDir, 'ultra-test-report-v1',
1452
1647
  );
1453
1648
  assertReportSchema(report, 'test');
1649
+ const verificationProfile = validateVerificationProfile(report);
1454
1650
  if (report.change_id !== run.change_id) {
1455
1651
  throw new WorkflowStateError('WORKFLOW_REPORT_CHANGE_MISMATCH', 'test report change_id is not bound to the workflow');
1456
1652
  }
@@ -1497,9 +1693,7 @@ function validateTestCompletion(db, run, rootDir) {
1497
1693
  && report.public_seams.every((item) => item?.status === 'pass')
1498
1694
  && report.failures.every((item) => item.status === 'resolved')
1499
1695
  && report.recovery.every((item) => item.status === 'pass')
1500
- && Object.values(report.verification_dimensions || {}).every(
1501
- (item) => ['pass', 'not_applicable'].includes(item?.status),
1502
- )
1696
+ && Object.values(report.verification_dimensions || {}).every((item) => item?.status === 'pass')
1503
1697
  && report.blocking_issues.length === 0;
1504
1698
  if (typeof report.passed !== 'boolean' || report.passed !== claimedPass) {
1505
1699
  throw new WorkflowStateError(
@@ -1517,12 +1711,21 @@ function validateTestCompletion(db, run, rootDir) {
1517
1711
  context_path: context.output.path,
1518
1712
  context_digest: context.output.digest,
1519
1713
  regression_signal: report.regression_signal,
1714
+ verification_profile: verificationProfile,
1520
1715
  verification_dimensions: report.verification_dimensions,
1521
1716
  blocking_issues: report.blocking_issues,
1522
1717
  };
1523
1718
  }
1524
1719
 
1525
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
+ ]);
1526
1729
  const REVIEW_SEVERITIES = new Set(['P0', 'P1', 'P2', 'P3']);
1527
1730
  const REVIEW_FINDING_FIELDS = [
1528
1731
  'id', 'axis', 'severity', 'category', 'title', 'file', 'line',
@@ -1566,6 +1769,7 @@ function readReviewSpecialists(run, rootDir) {
1566
1769
  const checkout = currentCheckout(rootDir);
1567
1770
  const expectedHead = checkout.head || 'workspace';
1568
1771
  const seenIds = new Set();
1772
+ const seenAgents = new Set();
1569
1773
  const artifacts = [];
1570
1774
  for (const [stepId, expectedAxis] of axes) {
1571
1775
  const outputs = run.steps.find((item) => item.step_id === stepId)?.outputs || [];
@@ -1595,6 +1799,13 @@ function readReviewSpecialists(run, rootDir) {
1595
1799
  'WORKFLOW_REPORT_INVALID', `${output.path} is not a complete ${expectedAxis} artifact`,
1596
1800
  );
1597
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);
1598
1809
  for (const finding of artifact.findings) {
1599
1810
  validateReviewFinding(finding, expectedAxis, seenIds, output.path);
1600
1811
  }
@@ -1605,6 +1816,8 @@ function readReviewSpecialists(run, rootDir) {
1605
1816
  }
1606
1817
 
1607
1818
  function assertCoordinatedReviewEvidence(report, specialists) {
1819
+ const sameSet = (left, right) => left.size === right.size
1820
+ && [...left].every((item) => right.has(item));
1608
1821
  const artifactPathsByAxis = Object.fromEntries([...REVIEW_AXES].map((axis) => [
1609
1822
  axis,
1610
1823
  specialists.artifacts.filter((artifact) => artifact.axis === axis).map((artifact) => artifact.path).sort(),
@@ -1625,15 +1838,12 @@ function assertCoordinatedReviewEvidence(report, specialists) {
1625
1838
  );
1626
1839
  }
1627
1840
  const completed = new Set(report.workers?.completed || []);
1628
- if (specialists.artifacts.some((artifact) => !completed.has(artifact.agent))
1841
+ const artifactAgents = new Set(specialists.artifacts.map((artifact) => artifact.agent));
1842
+ if (!sameSet(completed, artifactAgents)
1629
1843
  || (report.workers?.failed || []).length > 0) {
1630
1844
  throw new WorkflowStateError(
1631
- 'WORKFLOW_REVIEW_EVIDENCE_MISMATCH', 'review worker completion does not match specialist artifacts',
1632
- );
1633
- }
1634
- if ((report.workers?.skipped || []).length > 0 && (report.limitations || []).length === 0) {
1635
- throw new WorkflowStateError(
1636
- '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',
1637
1847
  );
1638
1848
  }
1639
1849
  const selection = Array.isArray(report.worker_selection) ? report.worker_selection : [];
@@ -1656,13 +1866,13 @@ function assertCoordinatedReviewEvidence(report, specialists) {
1656
1866
  ...(report.workers?.completed || []), ...(report.workers?.failed || []),
1657
1867
  ]);
1658
1868
  const expectedSkipped = new Set(report.workers?.skipped || []);
1659
- const sameSet = (left, right) => left.size === right.size && [...left].every((item) => right.has(item));
1660
- if (selection.length === 0 || !sameSet(selected, expectedSelected)
1869
+ if (!sameSet(seenWorkers, REVIEW_WORKERS)
1870
+ || !sameSet(selected, expectedSelected)
1661
1871
  || !sameSet(skipped, expectedSkipped)
1662
1872
  || !selected.has('review-spec') || !completed.has('review-spec')) {
1663
1873
  throw new WorkflowStateError(
1664
1874
  'WORKFLOW_REVIEW_EVIDENCE_MISMATCH',
1665
- 'review worker selection must match completed, failed, and skipped worker state',
1875
+ 'review worker selection must disposition the complete worker roster and match completed, failed, and skipped state',
1666
1876
  );
1667
1877
  }
1668
1878
 
@@ -1791,6 +2001,12 @@ function validateDeliveryCompletion(db, run, rootDir) {
1791
2001
  const { value: report, output } = readOutputJson(
1792
2002
  run, 'verify-delivery', rootDir, 'ultra-delivery-report-v1',
1793
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
+ }
1794
2010
  assertReportSchema(report, 'delivery');
1795
2011
  if (report.change_id !== run.change_id || report.archive_status !== 'archived') {
1796
2012
  throw new WorkflowStateError(
@@ -1809,24 +2025,6 @@ function validateDeliveryCompletion(db, run, rootDir) {
1809
2025
  || report.checks.some((item) => item?.status !== 'pass')) {
1810
2026
  throw new WorkflowStateError('WORKFLOW_DELIVERY_CHECK_FAILED', 'delivery checks must all pass');
1811
2027
  }
1812
- if (!report.release || typeof report.release !== 'object'
1813
- || typeof report.release.authorized !== 'boolean'
1814
- || typeof report.release.performed !== 'boolean') {
1815
- throw new WorkflowStateError('WORKFLOW_REPORT_INVALID', 'delivery report release decision is incomplete');
1816
- }
1817
- if (report.release.authorized !== report.release.performed) {
1818
- throw new WorkflowStateError(
1819
- 'WORKFLOW_RELEASE_INCOMPLETE', 'an authorized release must complete; an unauthorized release must not run',
1820
- );
1821
- }
1822
- const releaseStep = run.steps.find((item) => item.step_id === 'release-if-authorized');
1823
- const releaseDecision = validateReleaseAuthorizationDecision(releaseStep?.decisions || []);
1824
- if (report.release.authorized !== releaseDecision.authorized) {
1825
- throw new WorkflowStateError(
1826
- 'WORKFLOW_RELEASE_AUTHORITY_MISMATCH',
1827
- 'delivery report release claim does not match the recorded owner authorization decision',
1828
- );
1829
- }
1830
2028
  if (!String(report.rollback || '').trim()) {
1831
2029
  throw new WorkflowStateError('WORKFLOW_REPORT_INVALID', 'delivery report rollback guidance is required');
1832
2030
  }
@@ -1842,7 +2040,6 @@ function validateDeliveryCompletion(db, run, rootDir) {
1842
2040
  baseline_status: baseline.status,
1843
2041
  git_commit: checkout.head,
1844
2042
  worktree_digest: checkout.digest,
1845
- release: report.release,
1846
2043
  report_path: output.path,
1847
2044
  report_digest: output.digest,
1848
2045
  context_path: context.output.path,
@@ -1902,24 +2099,6 @@ function validatePlanArtifact(db, run, rootDir) {
1902
2099
  };
1903
2100
  }
1904
2101
 
1905
- function advanceLinkedChangeAfterPlan(db, run, rootDir) {
1906
- if (run.kind !== 'plan' || !run.change_id) return;
1907
- const linked = db.prepare(
1908
- `SELECT id FROM workflow_runs
1909
- WHERE kind = 'change' AND change_id = ? AND current_step = 'plan-change'
1910
- AND status IN ('active', 'blocked')
1911
- ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
1912
- ).get(run.change_id);
1913
- if (!linked) return;
1914
- recordWorkflowStep(db, {
1915
- id: linked.id, step_id: 'plan-change', status: 'completed',
1916
- evidence: [{
1917
- kind: 'workflow', ref: run.id,
1918
- summary: `Plan workflow ${run.id} completed with authoritative task contracts.`,
1919
- }],
1920
- }, { rootDir });
1921
- }
1922
-
1923
2102
  function completeWorkflow(db, input = {}, { rootDir = process.cwd() } = {}) {
1924
2103
  const id = nonEmpty(input.id, 'id');
1925
2104
  return ops.tx(db, () => {
@@ -1929,6 +2108,11 @@ function completeWorkflow(db, input = {}, { rootDir = process.cwd() } = {}) {
1929
2108
  if (run.status !== 'ready') {
1930
2109
  throw new WorkflowStateError('WORKFLOW_NOT_READY', `workflow ${id} is ${run.status}`);
1931
2110
  }
2111
+ decisionDialogue.assertDecisionGate(db, {
2112
+ workflow_run_id: run.id,
2113
+ change_id: run.change_id,
2114
+ baseline_id: run.baseline_id,
2115
+ }, { rootDir });
1932
2116
  if (run.artifact_health.status !== 'pass') {
1933
2117
  throw new WorkflowStateError('WORKFLOW_OUTPUT_STALE', 'workflow outputs are missing or stale', run.artifact_health);
1934
2118
  }
@@ -1961,8 +2145,9 @@ function completeWorkflow(db, input = {}, { rootDir = process.cwd() } = {}) {
1961
2145
  task_contract_digests: contract.task_contract_digests,
1962
2146
  profile: contract.profile,
1963
2147
  coverage: contract.coverage,
2148
+ authority_basis: 'accepted_change_contract',
1964
2149
  };
1965
- validatePlanApproval(approval);
2150
+ if (approval !== null) validatePlanApproval(approval);
1966
2151
  }
1967
2152
  if (run.kind === 'dev' && ops.readTask(db, run.task_id)?.status !== 'completed') {
1968
2153
  throw new WorkflowStateError(
@@ -1979,7 +2164,7 @@ function completeWorkflow(db, input = {}, { rootDir = process.cwd() } = {}) {
1979
2164
  type: 'workflow_completed', task_id: run.task_id, change_id: run.change_id,
1980
2165
  payload: { workflow_id: id, kind: run.kind, baseline_id: run.baseline_id },
1981
2166
  });
1982
- if (run.kind === 'plan') {
2167
+ if (run.kind === 'plan' && approval !== null) {
1983
2168
  ops.appendEventInTx(db, {
1984
2169
  type: 'plan_approved', change_id: run.change_id,
1985
2170
  payload: {
@@ -1992,7 +2177,6 @@ function completeWorkflow(db, input = {}, { rootDir = process.cwd() } = {}) {
1992
2177
  },
1993
2178
  });
1994
2179
  }
1995
- advanceLinkedChangeAfterPlan(db, run, rootDir);
1996
2180
  return readWorkflow(db, id, { rootDir });
1997
2181
  });
1998
2182
  }
@@ -2051,6 +2235,12 @@ function validatePlanContract(db, run, rootDir = process.cwd()) {
2051
2235
  );
2052
2236
  }
2053
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
+ }
2054
2244
  return task;
2055
2245
  });
2056
2246
  const changeRow = db.prepare(
@@ -2115,88 +2305,6 @@ function validatePlanContract(db, run, rootDir = process.cwd()) {
2115
2305
  };
2116
2306
  }
2117
2307
 
2118
- function activeInitializationRun(db, baselineId) {
2119
- return db.prepare(
2120
- `SELECT * FROM workflow_runs
2121
- WHERE kind = 'init' AND baseline_id = ? AND status IN ('active', 'blocked', 'ready')
2122
- ORDER BY updated_at DESC, rowid DESC LIMIT 1`,
2123
- ).get(baselineId);
2124
- }
2125
-
2126
- function blockInitializationInTx(db, baselineId, blockers = []) {
2127
- const run = activeInitializationRun(db, baselineId);
2128
- if (!run) return null;
2129
- const normalized = [...new Set(blockers.map((item) => String(item).trim()).filter(Boolean))];
2130
- const ts = nowIso();
2131
- db.prepare(
2132
- `UPDATE workflow_steps SET status = 'blocked', blockers_json = ?,
2133
- started_at = COALESCE(started_at, ?), completed_at = NULL, updated_at = ?
2134
- WHERE run_id = ? AND step_id = 'establish-baseline'`,
2135
- ).run(JSON.stringify(normalized), ts, ts, run.id);
2136
- db.prepare(
2137
- `UPDATE workflow_runs SET status = 'blocked', current_step = 'establish-baseline',
2138
- blockers_json = ?, updated_at = ? WHERE id = ?`,
2139
- ).run(JSON.stringify(normalized), ts, run.id);
2140
- ops.appendEventInTx(db, {
2141
- type: 'workflow_blocked',
2142
- payload: {
2143
- workflow_id: run.id, kind: 'init', step_id: 'establish-baseline',
2144
- status: 'blocked', blockers: normalized,
2145
- },
2146
- });
2147
- return run.id;
2148
- }
2149
-
2150
- function completeInitializationInTx(db, baseline) {
2151
- const run = activeInitializationRun(db, baseline.id);
2152
- if (!run) return null;
2153
- const ts = nowIso();
2154
- const steps = [
2155
- {
2156
- id: 'establish-baseline',
2157
- evidence: [{
2158
- kind: 'baseline', ref: baseline.id,
2159
- summary: `Baseline ${baseline.id} converged at revision ${baseline.repository_revision}.`,
2160
- }],
2161
- },
2162
- {
2163
- id: 'verify-initialization',
2164
- evidence: [{
2165
- kind: 'state', ref: '.ultra/state.db',
2166
- summary: 'Baseline authority, workflow state, and the post-initialization route are ready.',
2167
- }],
2168
- },
2169
- ];
2170
- for (const item of steps) {
2171
- db.prepare(
2172
- `UPDATE workflow_steps SET status = 'completed', evidence_json = ?, blockers_json = '[]',
2173
- started_at = COALESCE(started_at, ?), completed_at = ?, updated_at = ?
2174
- WHERE run_id = ? AND step_id = ?`,
2175
- ).run(JSON.stringify(item.evidence), ts, ts, ts, run.id, item.id);
2176
- ops.appendEventInTx(db, {
2177
- type: 'workflow_step_updated',
2178
- payload: { workflow_id: run.id, kind: 'init', step_id: item.id, status: 'completed' },
2179
- });
2180
- }
2181
- db.prepare(
2182
- `UPDATE workflow_runs SET status = 'completed', current_step = NULL, blockers_json = '[]',
2183
- approval_json = ?, summary_json = ?, completed_at = ?, updated_at = ? WHERE id = ?`,
2184
- ).run(
2185
- JSON.stringify({ approved_by: baseline.approved_by, approved_at: baseline.converged_at }),
2186
- JSON.stringify({
2187
- baseline_id: baseline.id,
2188
- repository_revision: baseline.repository_revision,
2189
- research_run_id: baseline.research_run_id,
2190
- }),
2191
- ts, ts, run.id,
2192
- );
2193
- ops.appendEventInTx(db, {
2194
- type: 'workflow_completed',
2195
- payload: { workflow_id: run.id, kind: 'init', baseline_id: baseline.id },
2196
- });
2197
- return run.id;
2198
- }
2199
-
2200
2308
  function inspectWorkflowHealth(db, { rootDir = process.cwd() } = {}) {
2201
2309
  const all = listWorkflows(db, { limit: 500 }, { rootDir });
2202
2310
  const active = all
@@ -2282,11 +2390,11 @@ module.exports = {
2282
2390
  listWorkflows,
2283
2391
  recordWorkflowStep,
2284
2392
  completeWorkflow,
2285
- blockInitializationInTx,
2286
- completeInitializationInTx,
2287
2393
  inspectWorkflowHealth,
2288
2394
  recoverUntrackedChangeWorkflows,
2289
2395
  validateDeliveryPrerequisites,
2290
2396
  validatePlanContract,
2397
+ assertCurrentPlan,
2398
+ assertApprovedReview,
2291
2399
  resolveProjectSourceRef: sourceRefFile,
2292
2400
  };