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.
Files changed (64) hide show
  1. package/.ultra-template/specs/research-distillate.md +4 -3
  2. package/CHANGELOG.md +86 -0
  3. package/README.md +54 -24
  4. package/adapters/_shared/codex-assets.cjs +10 -5
  5. package/adapters/_shared/handbook.cjs +1 -1
  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/docs/AGENT-CONTEXT.md +11 -8
  19. package/docs/ARCHITECTURE.md +67 -9
  20. package/docs/LEGACY-CLI-CROSSWALK.md +163 -0
  21. package/docs/PLAN.zh-CN.md +19 -13
  22. package/docs/RUNTIME-COMPAT-MATRIX.md +2 -2
  23. package/docs/WORKFLOW-LIFECYCLE.md +123 -59
  24. package/hooks/pre_stop_check.py +3 -2
  25. package/mcp-server/lib/baseline-workflow.cjs +84 -16
  26. package/mcp-server/lib/change-workflow.cjs +55 -35
  27. package/mcp-server/lib/context-spine.cjs +171 -105
  28. package/mcp-server/lib/git-bootstrap.cjs +240 -0
  29. package/mcp-server/lib/init-project.cjs +178 -41
  30. package/mcp-server/lib/project-breadcrumb.cjs +14 -14
  31. package/mcp-server/lib/state-db.cjs +257 -10
  32. package/mcp-server/lib/state-ops.cjs +64 -1
  33. package/mcp-server/lib/workflow-state.cjs +300 -205
  34. package/mcp-server/server.cjs +58 -34
  35. package/orchestrator/auto-merge.cjs +19 -0
  36. package/orchestrator/daemon.cjs +104 -0
  37. package/orchestrator/parallel-orchestrator.cjs +371 -49
  38. package/orchestrator/session-runner.cjs +354 -25
  39. package/package.json +2 -1
  40. package/skills/ultra-change/SKILL.md +69 -94
  41. package/skills/ultra-deliver/SKILL.md +37 -65
  42. package/skills/ultra-dev/SKILL.md +50 -52
  43. package/skills/ultra-doctor/SKILL.md +34 -49
  44. package/skills/ultra-init/SKILL.md +78 -105
  45. package/skills/ultra-plan/SKILL.md +78 -99
  46. package/skills/ultra-research/SKILL.md +99 -113
  47. package/skills/ultra-review/SKILL.md +38 -53
  48. package/skills/ultra-review/references/review-modes.md +5 -3
  49. package/skills/ultra-review/references/unified-schema.md +26 -6
  50. package/skills/ultra-review/scripts/review_wait.py +12 -0
  51. package/skills/ultra-status/SKILL.md +34 -44
  52. package/skills/ultra-test/SKILL.md +61 -54
  53. package/skills/ultra-think/SKILL.md +37 -51
  54. package/skills/ultra-think/references/decision-dialogue.md +87 -111
  55. package/spec/cli-protocol.md +35 -0
  56. package/spec/mcp-tools.yaml +145 -28
  57. package/spec/schemas/delivery-report.v1.schema.json +1 -46
  58. package/spec/schemas/state-db.sql +8 -2
  59. package/spec/schemas/test-report.v1.schema.json +45 -14
  60. package/ultra-tools/cli.cjs +1 -1
  61. package/ultra-tools/commands/session.cjs +10 -3
  62. package/ultra-tools/commands/status.cjs +19 -14
  63. package/ultra-tools/commands/system.cjs +2 -2
  64. 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 = '1.1';
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('establish-baseline', 'Record evidence and establish the project baseline', { evidence_required: true }),
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 focused, regression, static, build, and recovery checks', { evidence_required: true }),
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 route the next action', { evidence_required: true }),
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 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 }),
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('release-if-authorized', 'Release only when explicitly authorized'),
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 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
+ }
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' && (input.mode !== undefined || input.selected_steps !== undefined)) {
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 selected_steps are valid only for research workflows',
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(selectedResearchSteps(
736
- disposition.mode, disposition.selected_steps,
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
- ? 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)
878
965
  : definition.map((item) => item.id));
879
966
  let metadata = input.kind === 'research'
880
- ? { ...(input.metadata || {}), selected_steps: [...selected] }
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
- required ? 'pending' : 'skipped', required ? null : `Excluded by ${mode} mode.`, ts,
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 === '2.0' && value?.snapshot_id) {
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 v2 output`,
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 approvedDigests = plan.summary.task_contract_digests;
1243
- 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)) {
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 (approvedDigests[task.id] !== taskPlanDigest(task)) {
1355
+ if (task.stale) {
1250
1356
  throw new WorkflowStateError(
1251
- '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`,
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 = 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);
1313
1427
  if (tasks.length === 0) {
1314
- 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
+ );
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 { plan, taskIds } = assertCurrentPlan(db, run.change_id, rootDir);
1392
- 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
+ };
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
- 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)
1637
1843
  || (report.workers?.failed || []).length > 0) {
1638
1844
  throw new WorkflowStateError(
1639
- 'WORKFLOW_REVIEW_EVIDENCE_MISMATCH', 'review worker completion does not match specialist artifacts',
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
- const sameSet = (left, right) => left.size === right.size && [...left].every((item) => right.has(item));
1668
- if (selection.length === 0 || !sameSet(selected, expectedSelected)
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 worker state',
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
  };