okstra 0.172.0 → 0.173.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 (83) hide show
  1. package/README.md +8 -6
  2. package/docs/architecture/storage-model.md +11 -0
  3. package/docs/architecture.md +16 -14
  4. package/docs/cli.md +36 -5
  5. package/docs/performance-improvement-plan-v2.md +6 -5
  6. package/docs/project-structure-overview.md +21 -13
  7. package/docs/task-process/README.md +5 -3
  8. package/docs/task-process/error-analysis.md +2 -2
  9. package/docs/task-process/final-verification.md +2 -2
  10. package/docs/task-process/implementation-option-selection.md +70 -0
  11. package/docs/task-process/implementation-planning.md +23 -15
  12. package/docs/task-process/requirements-discovery.md +2 -2
  13. package/package.json +1 -1
  14. package/runtime/BUILD.json +2 -2
  15. package/runtime/agents/workers/report-writer-worker.md +30 -6
  16. package/runtime/bin/lib/okstra/cli.sh +5 -1
  17. package/runtime/bin/lib/okstra/globals.sh +1 -0
  18. package/runtime/bin/lib/okstra/usage.sh +3 -0
  19. package/runtime/bin/okstra.sh +2 -0
  20. package/runtime/prompts/duties/direction-selection-worker.md +44 -0
  21. package/runtime/prompts/duties/planning-worker.md +12 -4
  22. package/runtime/prompts/lead/context-loader.md +1 -1
  23. package/runtime/prompts/lead/convergence.md +5 -5
  24. package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
  25. package/runtime/prompts/lead/plan-body-verification.md +20 -3
  26. package/runtime/prompts/lead/report-writer.md +27 -5
  27. package/runtime/prompts/profiles/_common-contract.md +1 -1
  28. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  29. package/runtime/prompts/profiles/error-analysis.md +3 -3
  30. package/runtime/prompts/profiles/final-verification.md +3 -3
  31. package/runtime/prompts/profiles/forbidden-actions.json +7 -0
  32. package/runtime/prompts/profiles/implementation-option-selection.md +35 -0
  33. package/runtime/prompts/profiles/implementation-planning.md +50 -38
  34. package/runtime/prompts/profiles/implementation.md +2 -1
  35. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  36. package/runtime/prompts/profiles/requirements-discovery.md +3 -3
  37. package/runtime/prompts/wizard/prompts.ko.json +9 -1
  38. package/runtime/python/okstra_ctl/agent_invocation.py +1 -0
  39. package/runtime/python/okstra_ctl/analysis_packet.py +6 -0
  40. package/runtime/python/okstra_ctl/exact_coverage.py +128 -0
  41. package/runtime/python/okstra_ctl/fix_cycles.py +3 -1
  42. package/runtime/python/okstra_ctl/implementation_direction.py +836 -0
  43. package/runtime/python/okstra_ctl/implementation_options.py +479 -0
  44. package/runtime/python/okstra_ctl/plan_items.py +51 -3
  45. package/runtime/python/okstra_ctl/render.py +1 -0
  46. package/runtime/python/okstra_ctl/render_final_report.py +1 -0
  47. package/runtime/python/okstra_ctl/report_contract.py +45 -13
  48. package/runtime/python/okstra_ctl/report_html/render.py +4 -2
  49. package/runtime/python/okstra_ctl/report_html/router.py +4 -0
  50. package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +32 -0
  51. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +25 -10
  52. package/runtime/python/okstra_ctl/report_views.py +148 -12
  53. package/runtime/python/okstra_ctl/run.py +350 -2
  54. package/runtime/python/okstra_ctl/scope_provenance.py +15 -9
  55. package/runtime/python/okstra_ctl/user_response.py +75 -0
  56. package/runtime/python/okstra_ctl/wizard.py +144 -0
  57. package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
  58. package/runtime/python/okstra_ctl/workflow.py +29 -7
  59. package/runtime/schemas/final-report-v2.0.schema.json +1428 -137
  60. package/runtime/templates/reports/final-report-v2.template.md +4 -0
  61. package/runtime/templates/reports/final-verification-input.template.md +1 -1
  62. package/runtime/templates/reports/html/base.template.html +3 -2
  63. package/runtime/templates/reports/html/i18n/en.json +21 -1
  64. package/runtime/templates/reports/html/i18n/ko.json +21 -1
  65. package/runtime/templates/reports/html/macros/forms.html +21 -2
  66. package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +49 -0
  67. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +36 -2
  68. package/runtime/templates/reports/i18n/en.json +13 -0
  69. package/runtime/templates/reports/implementation-input.template.md +4 -2
  70. package/runtime/templates/reports/implementation-planning-input.template.md +18 -4
  71. package/runtime/templates/reports/improvement-discovery-input.template.md +1 -1
  72. package/runtime/templates/reports/md/tasks/implementation-option-selection.template.md +13 -0
  73. package/runtime/templates/reports/md/tasks/implementation-planning.template.md +17 -0
  74. package/runtime/templates/reports/report.js +111 -4
  75. package/runtime/templates/reports/task-brief.template.md +9 -3
  76. package/runtime/templates/reports/user-response.template.md +25 -4
  77. package/runtime/templates/worker-prompt-preamble.md +8 -0
  78. package/runtime/validators/validate-implementation-plan-stages.py +106 -1
  79. package/runtime/validators/validate-report-views.py +2 -2
  80. package/runtime/validators/validate-run.py +135 -25
  81. package/runtime/validators/validate_improvement_report.py +5 -1
  82. package/src/commands/execute/codex-run.mjs +1 -0
  83. package/src/commands/execute/render-bundle.mjs +1 -0
@@ -60,9 +60,14 @@ taskType: "{{FM_TASK_TYPE}}"
60
60
  - What is the visible symptom?
61
61
  - What are the expected vs actual results?
62
62
  - What are the current hypotheses and missing evidence?
63
+ - If `Task Type` is `implementation-option-selection`:
64
+ - Is this a comparison among possible directions or validation of one preselected direction?
65
+ - Which stable `EB-NNN`, `PB-NNN`, and `EO-NNN` requirements form the coverage denominator?
66
+ - Which mechanism, architecture boundary, preservation rule, or implementation constraint must each direction address?
63
67
  - If `Task Type` is `implementation-planning`:
64
- - What implementation options are under consideration?
65
- - What trade-offs, dependencies, or migrations matter?
68
+ - Which validated selection report and `selectedDirectionRef` authorise this new plan?
69
+ - Which mechanism, architecture boundary, invariants, and user constraints must the plan preserve?
70
+ - Which dependencies or migrations matter while realizing that one direction?
66
71
  - What validation and rollback approach is expected?
67
72
  - If `Task Type` is `implementation`:
68
73
  - Which approved `implementation-planning` final report authorises this run, and is its frontmatter `approved: true` cited verbatim?
@@ -141,6 +146,7 @@ taskType: "{{FM_TASK_TYPE}}"
141
146
  - Allowed and forbidden actions for each task type are listed in `Lifecycle Phase Boundaries` of the okstra skill (`prompts/lead/okstra-lead-contract.md`). The lead and every worker stay inside that boundary.
142
147
  - "proceed to the next step" or any equivalent user phrase is interpreted as "complete the remaining outputs of the current phase," never as "start the next lifecycle phase." The next phase begins only via a fresh okstra invocation with the new `--task-type`.
143
148
  - For `implementation-planning` specifically: produce a plan document with the sections listed in `okstra-implementation-planning-input.template.md` `## Required Plan Deliverable`. Do not edit project source code, run builds/migrations/deployments, or write artifacts outside the run's own directories.
149
+ - For `implementation-option-selection` specifically: compare or validate implementation directions without editing source, running tests/builds, or writing exact file lists, stage maps, and test commands. Only directions with 100% requirement coverage and 100% scope precision may be displayed.
144
150
  - For `implementation` specifically: edits are bounded by the approved plan's file list (the `--approved-plan` reference). The run MUST refuse to start if the approved plan path is missing or its frontmatter `approved` field is not `true`. `git push`, publish, deploy, real migrations, and any third-party write API call remain forbidden; only local `git add`/`git commit` are allowed. Verifier roles stay read-only — they record fix recommendations rather than applying edits — and acceptance verdicts belong to `final-verification`, not this phase.
145
151
 
146
152
  ## Available MCP Servers
@@ -157,7 +163,7 @@ How to invoke (worker-by-worker):
157
163
 
158
164
  Usage policy:
159
165
 
160
- - **Allowed phases**: `requirements-discovery`, `error-analysis`, `implementation-planning`, `final-verification`. Use only when local schema/data evidence improves the answer. Always cite the server, table, and the SELECT used as evidence in worker output.
166
+ - **Allowed phases**: `requirements-discovery`, `error-analysis`, `implementation-option-selection`, `implementation-planning`, `final-verification`. Use only when local schema/data evidence improves the answer. Always cite the server, table, and the SELECT used as evidence in worker output.
161
167
  - **`implementation` phase**: read-only MCP queries are permitted as cross-checks; MCP MUST NOT be used as a write path even if a write tool becomes available — schema/data mutations belong in the codebase migration files reviewed by humans.
162
168
  - **CLI-wrapper workers**: can use these MCP servers only if their provider CLI configuration mirrors the same servers. If not configured, the worker should record `MCP not available in this CLI` in `Missing Information or Assumptions` rather than guessing.
163
169
  - **Forbidden**: connecting to non-listed databases, running anything that mutates state (server is read-only — flagged write attempts are a contract violation), persisting query results outside the run's own artifact directories.
@@ -9,9 +9,11 @@ This file defines the standard format of the markdown produced by the **Export u
9
9
 
10
10
  ```yaml
11
11
  task-key: <task-group>/<task-id>
12
- task-type: <requirements-discovery | error-analysis | implementation-planning | implementation | final-verification | release-handoff | project-analysis | feature-analysis | change-impact-analysis>
12
+ task-type: <requirements-discovery | error-analysis | implementation-option-selection | implementation-planning | implementation | final-verification | release-handoff | improvement-discovery | project-analysis | feature-analysis | change-impact-analysis>
13
13
  seq: <3-digit zero-padded run sequence>
14
14
  source-report: <project-relative path to the final-report .md the HTML was derived from>
15
+ source-data: <project-relative path to the final-report data.json; omit for legacy reports without one>
16
+ source-data-sha256: <SHA-256 of the exact source-data bytes; omit when source-data is omitted>
15
17
  created-by: user
16
18
  created-at: <ISO 8601 UTC timestamp>
17
19
  ```
@@ -72,15 +74,16 @@ When you pick a verdict in the Plan Decision widget and Export, the following bl
72
74
  ```markdown
73
75
  ## PLAN DECISION
74
76
  - Status: <approved | revision-requested | rejected>
75
- - Implementation-Option: <the name exactly as in Option Candidates>
77
+ - Implementation-Option: <legacy plans only: the name exactly as in Option Candidates>
76
78
  - Reason:
77
79
  > <one quoted line per input line>
78
80
  ```
79
81
 
80
82
  - `revision-requested` and `rejected` require a reason: a plan sent back without one leaves the next run guessing at what to change, so Export refuses to serialise it.
81
- - `Implementation-Option:` is emitted only for `approved`, and only when you moved off the recommended default — otherwise implementation falls back to the plan's Recommended Option.
83
+ - A `planningContract: selected-direction` plan never emits `Implementation-Option:`. The direction was confirmed before planning, and this block decides only whether the detailed plan is approved, revised, or rejected.
84
+ - A legacy candidate plan emits `Implementation-Option:` only for `approved`, and only when you moved off the recommended default. Otherwise implementation falls back to the legacy plan's Recommended Option.
82
85
  - `Reason:` is optional for `approved`, and the line is omitted when empty.
83
- - The consumer of an `approved` block is the approve-confirm step of the implementation start wizard (`scripts/okstra_ctl/wizard.py`). After user confirmation the wizard applies it through the existing `--approve` / `--implementation-option` path; the sidecar itself does not bypass approval-gate validation. A non-approved status is ignored there — that step only ever looks for an approval.
86
+ - The consumer of an `approved` block is the approve-confirm step of the implementation start wizard (`scripts/okstra_ctl/wizard.py`). After user confirmation the wizard applies `--approve`; it applies `--implementation-option` only for a legacy plan. The sidecar itself does not bypass approval-gate validation. A non-approved status is ignored there — that step only ever looks for an approval.
84
87
  - The parser is `parse_plan_decision` in `scripts/okstra_ctl/user_response.py`, and it accepts only the lowercase statuses that are byte-identical to the producer output (hand-edited values such as `Approved` are rejected fail-closed).
85
88
  - `--resume-clarification` attaches the sidecar verbatim, so the PLAN DECISION block reaches the next planning run — which is how a rejection and its reason get acted on.
86
89
 
@@ -102,6 +105,24 @@ The Analysis Review control appends one decision block. `revision-requested` and
102
105
 
103
106
  The block is the review decision's sole storage location. Export never changes the source final-report markdown. A later analysis rerun receives the sidecar through `--clarification-response` and records one `analysisReviewResolution` row for each imported affected ID.
104
107
 
108
+ ## DIRECTION SELECTION block (implementation-option-selection comparison only)
109
+
110
+ Selecting one ranked direction and checking `Confirmed` appends this block. `Selection-Note` and `Constraints` keep their field lines when empty, but do not emit empty quote lines.
111
+
112
+ ```markdown
113
+ ## DIRECTION SELECTION
114
+ - Status: selected
115
+ - Option-ID: IO-002
116
+ - Option-Name: Adapter boundary
117
+ - Confirmed: true
118
+ - Selection-Note:
119
+ > Use the existing port.
120
+ - Constraints:
121
+ > Keep PB-001 unchanged.
122
+ ```
123
+
124
+ The parser accepts exactly one block, lowercase `selected`, and lowercase `true`. A `preselected-validation` report displays its confirmed upstream direction read-only and does not export a new selection block.
125
+
105
126
  ## Compatibility Rules
106
127
 
107
128
  - If `Kind` has an unknown value, the form renders with a `<textarea>` fallback, and the received `Kind` string is preserved as-is during serialization.
@@ -61,6 +61,14 @@ Every analysis result starts with YAML frontmatter containing the task identity
61
61
 
62
62
  Every item has a worker-local ID and file:line evidence where code evidence exists. Sections 1–5 are the common core: feasibility, requirement interpretation, hidden assumptions, alternatives, and execution risk. Section 6 is the only legal home for specialization and is not consensus input.
63
63
 
64
+ ### Selected-direction planning ownership
65
+
66
+ For `implementation-planning` with `selected-direction.json`, sections 1–5 read that snapshot and the original requirements, then assess only its realization into files, interfaces, stages, validation, rollback, bidirectional requirement links, and `direction-invalidated` evidence. Direction selection remains upstream of this prompt.
67
+
68
+ ### Legacy candidate-comparison ownership
69
+
70
+ For an implementation-planning compatibility rerun without the snapshot, sections 1–5 retain candidate comparison, trade-off, recommendation, and `P-Opt-*` evidence.
71
+
64
72
  ### Ticket Tagging
65
73
 
66
74
  For `requirements-discovery`, `error-analysis`, and `implementation-planning`, tag every section 1–5 item with its related ticket. Use `Issue / Ticket`, fall back to Task ID, then `unknown`; comma-separate multiple tickets.
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
  import argparse
9
9
  import re
10
10
  import sys
11
+ from collections import Counter
11
12
  from dataclasses import dataclass
12
13
  from pathlib import Path
13
14
  from typing import List, Tuple
@@ -472,6 +473,99 @@ def _check_data_step_counts(
472
473
  return errs
473
474
 
474
475
 
476
+ def _check_data_stage_identities(
477
+ raw_stage_map: List[dict], stages: List[dict]
478
+ ) -> List[ValidationError]:
479
+ """Reject ambiguous schema-v2 identities before building stage indexes."""
480
+ errs: List[ValidationError] = []
481
+ stage_map_numbers = [
482
+ row["stage"]
483
+ for row in raw_stage_map
484
+ if isinstance(row, dict) and isinstance(row.get("stage"), int)
485
+ ]
486
+ stage_numbers = [
487
+ row["stage"]
488
+ for row in stages
489
+ if isinstance(row, dict) and isinstance(row.get("stage"), int)
490
+ ]
491
+ duplicate_stage_map = {
492
+ number for number, count in Counter(stage_map_numbers).items() if count > 1
493
+ }
494
+ duplicate_stages = {
495
+ number for number, count in Counter(stage_numbers).items() if count > 1
496
+ }
497
+ errs.extend(
498
+ ValidationError("S3", number, f"stageMap duplicate stage {number}")
499
+ for number in sorted(duplicate_stage_map)
500
+ )
501
+ errs.extend(
502
+ ValidationError("S3", number, f"stages[] duplicate stage {number}")
503
+ for number in sorted(duplicate_stages)
504
+ )
505
+
506
+ stage_map_set = set(stage_map_numbers)
507
+ stage_set = set(stage_numbers)
508
+ errs.extend(
509
+ ValidationError(
510
+ "S3",
511
+ number,
512
+ f"stageMap is missing stages[] stage {number}",
513
+ )
514
+ for number in sorted(stage_set - stage_map_set)
515
+ )
516
+ errs.extend(
517
+ ValidationError(
518
+ "S3",
519
+ number,
520
+ f"stages[] is missing stageMap stage {number}",
521
+ )
522
+ for number in sorted(stage_map_set - stage_set)
523
+ )
524
+
525
+ stage_map_by_number = {
526
+ row["stage"]: row
527
+ for row in raw_stage_map
528
+ if isinstance(row, dict)
529
+ and isinstance(row.get("stage"), int)
530
+ and row["stage"] not in duplicate_stage_map
531
+ }
532
+ stages_by_number = {
533
+ row["stage"]: row
534
+ for row in stages
535
+ if isinstance(row, dict)
536
+ and isinstance(row.get("stage"), int)
537
+ and row["stage"] not in duplicate_stages
538
+ }
539
+ errs.extend(
540
+ ValidationError(
541
+ "S3",
542
+ number,
543
+ f"stageMap and stages[] stage {number} title must match",
544
+ )
545
+ for number in sorted(stage_map_by_number.keys() & stages_by_number.keys())
546
+ if stage_map_by_number[number].get("title")
547
+ != stages_by_number[number].get("title")
548
+ )
549
+
550
+ for stage in stages:
551
+ number = stage.get("stage")
552
+ step_numbers = [
553
+ step["step"]
554
+ for step in stage.get("stepwiseExecution") or []
555
+ if isinstance(step, dict) and isinstance(step.get("step"), int)
556
+ ]
557
+ errs.extend(
558
+ ValidationError(
559
+ "S4",
560
+ number if isinstance(number, int) else 0,
561
+ f"stages[] stage {number} step {step} is duplicate",
562
+ )
563
+ for step, count in sorted(Counter(step_numbers).items())
564
+ if count > 1
565
+ )
566
+ return errs
567
+
568
+
475
569
  def collect_data_validation_errors(planning: dict) -> List[ValidationError]:
476
570
  """The S-checks that schema v2 cannot express, over `implementationPlanning`.
477
571
 
@@ -482,11 +576,22 @@ def collect_data_validation_errors(planning: dict) -> List[ValidationError]:
482
576
  covers presence and cardinality; this covers the relationships between
483
577
  fields, which is what a JSON Schema has no way to say.
484
578
  """
485
- stage_map, errors = _data_stage_metas(planning.get("stageMap") or [])
579
+ if (
580
+ planning.get("planningContract") == "selected-direction"
581
+ and planning.get("outcome") == "direction-invalidated"
582
+ ):
583
+ return []
584
+
585
+ raw_stage_map = planning.get("stageMap") or []
586
+ stage_map, errors = _data_stage_metas(raw_stage_map)
486
587
  stages = [s for s in (planning.get("stages") or []) if isinstance(s, dict)]
487
588
  if not stage_map and not stages:
488
589
  return errors
489
590
 
591
+ identity_errors = _check_data_stage_identities(raw_stage_map, stages)
592
+ errors.extend(identity_errors)
593
+ if identity_errors:
594
+ return errors
490
595
  errors.extend(_check_data_step_counts(stage_map, stages))
491
596
  errors.extend(_check_depends_on(stage_map))
492
597
  errors.extend(_report_shared_parallel_files({
@@ -43,7 +43,7 @@ from okstra_ctl.report_views import ( # noqa: E402
43
43
  )
44
44
  from okstra_ctl.report_view_artifacts import html_view_path # noqa: E402
45
45
  from okstra_ctl.final_report_paths import final_report_data_path # noqa: E402
46
- from okstra_ctl.report_contract import TASK_TYPE_REQUIRED_HUMAN_FIELDS # noqa: E402
46
+ from okstra_ctl.report_contract import required_human_fields_for_report # noqa: E402
47
47
 
48
48
 
49
49
  _EXTERNAL_URL_RE = re.compile(
@@ -98,7 +98,7 @@ def _validate_v2(report_path: Path, data_path: Path, data: dict) -> list[str]:
98
98
  if f'data-task-template="{task_type}"' not in html_text:
99
99
  failures.append(f"v2 html task template mismatch for {task_type}")
100
100
  actual_fields = set(re.findall(r'data-report-field="([^"]+)"', main_body))
101
- for field in TASK_TYPE_REQUIRED_HUMAN_FIELDS.get(task_type, ()):
101
+ for field in required_human_fields_for_report(task_type, data):
102
102
  if field not in actual_fields:
103
103
  failures.append(f"missing human field: {field}")
104
104
 
@@ -71,7 +71,12 @@ from okstra_ctl.incremental_scope import ( # noqa: E402
71
71
  coverage_row_blocked_on,
72
72
  stages_for_clarification,
73
73
  )
74
- from okstra_ctl.workflow import DEFAULT_NEXT_PHASE, PHASE_SEQUENCE # noqa: E402
74
+ from okstra_ctl.workflow import ( # noqa: E402
75
+ DEFAULT_NEXT_PHASE,
76
+ ERROR_ANALYSIS_ROUTING_DIRECTIONS,
77
+ PHASE_SEQUENCE,
78
+ REQUIREMENTS_DISCOVERY_ROUTING_TARGETS,
79
+ )
75
80
  from okstra_ctl.md_table import ( # noqa: E402
76
81
  is_separator_row as _is_markdown_separator,
77
82
  split_pipe_row as _split_pipe_row,
@@ -80,9 +85,16 @@ from okstra_ctl.final_report_paths import final_report_data_path as _data_path_f
80
85
  from okstra_ctl.improvement_assignment import ( # noqa: E402
81
86
  validate_primary_lens_assignments,
82
87
  )
88
+ from okstra_ctl.implementation_options import ( # noqa: E402
89
+ validate_implementation_option_selection,
90
+ )
91
+ from okstra_ctl.implementation_direction import ( # noqa: E402
92
+ validate_selected_direction_plan,
93
+ )
83
94
  from okstra_ctl.worker_prompt_policy import GRILLING_LOG_HEADER # noqa: E402
84
95
  from okstra_ctl.scope_provenance import ( # noqa: E402
85
96
  brief_citation_problem,
97
+ brief_end_state_id_sequence,
86
98
  brief_end_state_ids,
87
99
  brief_headings,
88
100
  parse_source,
@@ -406,7 +418,28 @@ def _error_analysis_next_phase(data: Mapping[str, Any]) -> str | None:
406
418
  if not isinstance(routing, Mapping):
407
419
  return None
408
420
  target = routing.get("nextTaskType")
409
- if target in {"error-analysis", "implementation-planning"}:
421
+ if target in ERROR_ANALYSIS_ROUTING_DIRECTIONS:
422
+ return str(target)
423
+ return None
424
+
425
+
426
+ def _requirements_discovery_next_phase(data: Mapping[str, Any]) -> str | None:
427
+ if not isinstance(data, Mapping):
428
+ return None
429
+ header = data.get("header")
430
+ if (
431
+ not isinstance(header, Mapping)
432
+ or header.get("taskType") != "requirements-discovery"
433
+ ):
434
+ return None
435
+ requirements = data.get("requirementsDiscovery")
436
+ if not isinstance(requirements, Mapping):
437
+ return None
438
+ routing = requirements.get("routing")
439
+ if not isinstance(routing, Mapping):
440
+ return None
441
+ target = routing.get("nextTaskType")
442
+ if target in REQUIREMENTS_DISCOVERY_ROUTING_TARGETS:
410
443
  return str(target)
411
444
  return None
412
445
 
@@ -445,11 +478,12 @@ def update_workflow_metadata(
445
478
  # Validation just passed → actively advance to the next phase in
446
479
  # the sequence rather than preserving a stale value that may equal
447
480
  # current_phase (which would cause the lifecycle pointer to stall).
448
- report_next_phase = (
449
- _error_analysis_next_phase(report_data or {})
450
- if current_phase == "error-analysis"
451
- else None
452
- )
481
+ if current_phase == "requirements-discovery":
482
+ report_next_phase = _requirements_discovery_next_phase(report_data or {})
483
+ elif current_phase == "error-analysis":
484
+ report_next_phase = _error_analysis_next_phase(report_data or {})
485
+ else:
486
+ report_next_phase = None
453
487
  next_recommended_phase = report_next_phase or advance_next_phase(
454
488
  current_phase, phase_sequence
455
489
  )
@@ -3142,10 +3176,15 @@ def _validate_error_analysis_consistency(
3142
3176
  target = routing.get("nextTaskType")
3143
3177
  leading_cause_id = routing.get("leadingCauseId")
3144
3178
  candidate_id_set = set(candidate_ids)
3145
- if target == "implementation-planning":
3179
+ if isinstance(target, str) and target not in ERROR_ANALYSIS_ROUTING_DIRECTIONS:
3180
+ failures.append(
3181
+ "final-report data.json: errorAnalysis.routing has unsupported "
3182
+ f"routing target `{target}`."
3183
+ )
3184
+ if target == "implementation-option-selection":
3146
3185
  if not candidates:
3147
3186
  failures.append(
3148
- "final-report data.json: implementation-planning routing requires "
3187
+ "final-report data.json: implementation-option-selection routing requires "
3149
3188
  "at least one cause candidate."
3150
3189
  )
3151
3190
  if (
@@ -3153,7 +3192,7 @@ def _validate_error_analysis_consistency(
3153
3192
  or leading_cause_id not in candidate_id_set
3154
3193
  ):
3155
3194
  failures.append(
3156
- "final-report data.json: implementation-planning routing "
3195
+ "final-report data.json: implementation-option-selection routing "
3157
3196
  "leadingCauseId must reference a cause candidate."
3158
3197
  )
3159
3198
  elif target == "error-analysis" and (
@@ -3165,10 +3204,7 @@ def _validate_error_analysis_consistency(
3165
3204
  "empty or reference a cause candidate."
3166
3205
  )
3167
3206
 
3168
- expected_direction = {
3169
- "implementation-planning": "begin-planning",
3170
- "error-analysis": "continue-investigation",
3171
- }.get(target)
3207
+ expected_direction = ERROR_ANALYSIS_ROUTING_DIRECTIONS.get(target)
3172
3208
  verdict_card_value = data.get("verdictCard")
3173
3209
  verdict_card = (
3174
3210
  verdict_card_value if isinstance(verdict_card_value, Mapping) else {}
@@ -3230,7 +3266,7 @@ def _validate_error_analysis_consistency(
3230
3266
 
3231
3267
  if isinstance(target, str) and target in {
3232
3268
  "error-analysis",
3233
- "implementation-planning",
3269
+ "implementation-option-selection",
3234
3270
  }:
3235
3271
  for field_name, value in (
3236
3272
  ("verdictCard.nextStep", verdict_card.get("nextStep")),
@@ -3373,7 +3409,25 @@ def validate_final_report_data(
3373
3409
 
3374
3410
  task_type = (data.get("header") or {}).get("taskType")
3375
3411
  _validate_verifier_fail_blocks_verdict(data, failures)
3376
- if task_type == "implementation":
3412
+ if task_type == "implementation-option-selection":
3413
+ selection = data.get("implementationOptionSelection") or {}
3414
+ validation_root = project_root or report_path.parent
3415
+ original_ids = brief_end_state_id_sequence(
3416
+ _brief_path_from_manifest(manifest, validation_root)
3417
+ )
3418
+ roster = manifest.get("recommendedWorkers") or ()
3419
+ participating_analysers = tuple(
3420
+ worker for worker in roster if worker != "report-writer"
3421
+ )
3422
+ failures.extend(
3423
+ f"implementation-option-selection: {error}"
3424
+ for error in validate_implementation_option_selection(
3425
+ selection,
3426
+ original_ids,
3427
+ participating_analysers,
3428
+ )
3429
+ )
3430
+ elif task_type == "implementation":
3377
3431
  _validate_stage_carry_sidecar_exists(data, report_path, failures)
3378
3432
  if task_type == "error-analysis":
3379
3433
  _validate_error_analysis_consistency(data, failures)
@@ -3382,6 +3436,31 @@ def validate_final_report_data(
3382
3436
  _validate_verified_row_recorded(data, report_path, failures)
3383
3437
  elif task_type == "implementation-planning":
3384
3438
  active_report_contracts = report_contracts or set()
3439
+ planning = data.get("implementationPlanning") or {}
3440
+ selected_direction_contract = (
3441
+ isinstance(planning, Mapping)
3442
+ and planning.get("planningContract") == "selected-direction"
3443
+ )
3444
+ if selected_direction_contract:
3445
+ validation_root = project_root or report_path.parent
3446
+ try:
3447
+ brief_path = _brief_path_from_manifest(manifest, validation_root)
3448
+ except (OSError, ValueError) as exc:
3449
+ failures.append(
3450
+ "implementation-planning selected-direction: run manifest "
3451
+ f"taskBriefPath is malformed: {exc}"
3452
+ )
3453
+ brief_path = validation_root / "__invalid-brief__"
3454
+ task_root = _task_root_from_run_dir(report_path.parent.parent)
3455
+ snapshot_path = task_root / "instruction-set" / "selected-direction.json"
3456
+ failures.extend(
3457
+ f"implementation-planning selected-direction: {error}"
3458
+ for error in validate_selected_direction_plan(
3459
+ data, brief_path, snapshot_path
3460
+ )
3461
+ )
3462
+ if planning.get("outcome") == "direction-invalidated":
3463
+ return data
3385
3464
  _validate_implementation_planning_cross_project(data, failures)
3386
3465
  _validate_implementation_planning_decision_drafts(data, failures)
3387
3466
  for warning in validate_plan_body_section(data, report_path, failures):
@@ -3401,8 +3480,9 @@ def validate_final_report_data(
3401
3480
  resolve_architecture(_project_root_from_report(report_path)),
3402
3481
  failures,
3403
3482
  )
3404
- _validate_requirement_deviations(data, failures)
3405
- _validate_requirement_coverage_covered_by(data, failures)
3483
+ if not selected_direction_contract:
3484
+ _validate_requirement_deviations(data, failures)
3485
+ _validate_requirement_coverage_covered_by(data, failures)
3406
3486
  warnings = _validate_design_prep_contract(
3407
3487
  data,
3408
3488
  report_path,
@@ -7803,6 +7883,13 @@ def _validate_stage_has_requirement(data: dict, failures: list[str]) -> None:
7803
7883
  )
7804
7884
 
7805
7885
 
7886
+ _FINAL_VERIFICATION_ROUTING_TOKEN_RE = re.compile(
7887
+ r"(?<![A-Za-z-])(?:release-handoff\(stage-group\)|release-handoff|done|"
7888
+ r"implementation|error-analysis|implementation-option-selection|"
7889
+ r"implementation-planning)(?![A-Za-z-])"
7890
+ )
7891
+
7892
+
7806
7893
  def _validate_final_verification_consistency(data: dict, failures: list[str]) -> None:
7807
7894
  """Enforce verdict ↔ blocker/condition/routing consistency on the
7808
7895
  final-verification data.json (SSOT). The schema guarantees field SHAPE;
@@ -7817,7 +7904,16 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
7817
7904
  fv = data.get("finalVerification") or {}
7818
7905
  blockers = fv.get("acceptanceBlockers") or []
7819
7906
  conditions = verdict.get("conditionalAcceptanceConditions") or []
7820
- routing = fv.get("routingRecommendation") or ""
7907
+ routing_value = fv.get("routingRecommendation")
7908
+ routing = routing_value if isinstance(routing_value, str) else ""
7909
+ routing_tokens = _FINAL_VERIFICATION_ROUTING_TOKEN_RE.findall(routing)
7910
+ routing_token = routing_tokens[0] if len(routing_tokens) == 1 else None
7911
+
7912
+ if routing_token is None:
7913
+ failures.append(
7914
+ "final-verification: routingRecommendation must contain exactly one "
7915
+ "supported routing token."
7916
+ )
7821
7917
 
7822
7918
  if token == "accepted" and blockers:
7823
7919
  failures.append(
@@ -7834,7 +7930,10 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
7834
7930
  "final-verification: verdict `conditional-accept` but "
7835
7931
  "conditionalAcceptanceConditions is empty — list every condition."
7836
7932
  )
7837
- if "release-handoff" in routing and token != "accepted":
7933
+ if (
7934
+ routing_token in {"release-handoff", "release-handoff(stage-group)"}
7935
+ and token != "accepted"
7936
+ ):
7838
7937
  failures.append(
7839
7938
  f"final-verification: routingRecommendation cites `release-handoff` "
7840
7939
  f"but verdict is `{token}` — release-handoff routing is allowed only "
@@ -7847,8 +7946,7 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
7847
7946
  f"final-verification: verificationScope must be `whole-task` or "
7848
7947
  f"`single-stage`, got {scope!r}."
7849
7948
  )
7850
- if (scope == "single-stage" and "release-handoff" in routing
7851
- and "release-handoff(stage-group)" not in routing):
7949
+ if scope == "single-stage" and routing_token == "release-handoff":
7852
7950
  failures.append(
7853
7951
  "final-verification: verificationScope `single-stage` cannot recommend "
7854
7952
  "plain release-handoff routing — a single-stage accepted verdict may "
@@ -9144,16 +9242,28 @@ def main() -> int:
9144
9242
  for warning in conformance_warnings:
9145
9243
  print(f"validate-run: warning: {warning}", file=sys.stderr)
9146
9244
  if task_type in _BRIEF_DERIVED_PHASES:
9147
- brief_path = _brief_path_from_manifest(task_manifest, project_root)
9245
+ planning = validation_data.get("implementationPlanning")
9246
+ selected_direction_plan = (
9247
+ task_type == "implementation-planning"
9248
+ and isinstance(planning, Mapping)
9249
+ and planning.get("planningContract") == "selected-direction"
9250
+ )
9251
+ brief_path = (
9252
+ project_root / "__selected-direction-brief-validated-from-run-manifest__"
9253
+ if selected_direction_plan
9254
+ else _brief_path_from_manifest(task_manifest, project_root)
9255
+ )
9148
9256
  if task_type in _END_STATE_PHASES:
9149
9257
  if task_type == "implementation-planning":
9150
9258
  _validate_planning_conformance_declared(report_path, failures)
9151
- _validate_end_state_coverage(validation_data, brief_path, failures)
9152
- if task_type == "implementation-planning":
9259
+ if not selected_direction_plan:
9260
+ _validate_end_state_coverage(validation_data, brief_path, failures)
9261
+ if task_type == "implementation-planning" and not selected_direction_plan:
9153
9262
  _validate_requirement_provenance(
9154
9263
  validation_data, brief_path, failures
9155
9264
  )
9156
9265
  _validate_stage_has_requirement(validation_data, failures)
9266
+ if task_type == "implementation-planning":
9157
9267
  _append_stage_data_failures(validation_data, failures)
9158
9268
  if task_type == "improvement-discovery":
9159
9269
  run_dir = report_path.parent.parent
@@ -34,7 +34,11 @@ from okstra_ctl.md_table import split_pipe_row
34
34
 
35
35
 
36
36
  _VERDICT_TOKENS = ("candidates-ready", "no-candidates", "blocked")
37
- _NEXT_PHASES = ("requirements-discovery", "implementation-planning", "error-analysis")
37
+ _NEXT_PHASES = (
38
+ "requirements-discovery",
39
+ "implementation-option-selection",
40
+ "error-analysis",
41
+ )
38
42
  _CAND_ID_RE = re.compile(r"^I-\d{3}$")
39
43
  _SOURCE_WORKER_RE = re.compile(r"^([a-z-]+):([A-Za-z0-9._-]+)$")
40
44
  _CONSENSUS_VALUES = ("full", "partial", "contested", "worker-unique")
@@ -16,6 +16,7 @@ Usage:
16
16
  [--antigravity-model <m>] [--report-writer-model <m>] \\
17
17
  [--related-tasks <list>] [--base-ref <ref>] \\
18
18
  [--clarification-response <path>] [--work-category <cat>] \\
19
+ [--selected-direction <selection-final-report.md>] \\
19
20
  [--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \\
20
21
  [--fix-cycle <yes|no>]
21
22
 
@@ -21,6 +21,7 @@ Usage:
21
21
  [--lead-runtime auto|<host-id-or-alias>] \\
22
22
  [--related-tasks <list>] [--base-ref <ref>] \\
23
23
  [--clarification-response <path>] [--work-category <cat>] \\
24
+ [--selected-direction <selection-final-report.md>] \\
24
25
  [--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \\
25
26
  [--fix-cycle <yes|no>]
26
27