okstra 0.175.1 → 0.176.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 (81) hide show
  1. package/docs/architecture/storage-model.md +2 -2
  2. package/docs/architecture.md +22 -19
  3. package/docs/cli.md +18 -14
  4. package/docs/for-ai/skills/okstra-inspect.md +2 -3
  5. package/docs/for-ai/skills/okstra-rollup.md +1 -0
  6. package/docs/project-structure-overview.md +57 -56
  7. package/docs/task-process/README.md +11 -9
  8. package/docs/task-process/common-flow.md +13 -16
  9. package/docs/task-process/error-analysis.md +9 -10
  10. package/docs/task-process/final-verification.md +7 -7
  11. package/docs/task-process/implementation-planning.md +9 -9
  12. package/docs/task-process/implementation.md +6 -6
  13. package/docs/task-process/release-handoff.md +8 -7
  14. package/docs/task-process/requirements-discovery.md +8 -8
  15. package/package.json +1 -1
  16. package/runtime/BUILD.json +2 -2
  17. package/runtime/bin/lib/okstra/interactive.sh +12 -6
  18. package/runtime/bin/lib/okstra/usage.sh +3 -2
  19. package/runtime/bin/okstra-spawn-followups.py +4 -2
  20. package/runtime/prompts/launch.template.md +2 -2
  21. package/runtime/prompts/lead/context-loader.md +1 -2
  22. package/runtime/prompts/lead/okstra-lead-contract.md +3 -4
  23. package/runtime/prompts/lead/report-writer.md +15 -1
  24. package/runtime/prompts/lead/team-contract.md +16 -12
  25. package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
  26. package/runtime/prompts/profiles/_implementation-executor.md +0 -3
  27. package/runtime/prompts/profiles/_implementation-verifier.md +3 -7
  28. package/runtime/prompts/profiles/change-impact-analysis.md +9 -5
  29. package/runtime/prompts/profiles/error-analysis.md +14 -8
  30. package/runtime/prompts/profiles/feature-analysis.md +9 -5
  31. package/runtime/prompts/profiles/final-verification.md +10 -7
  32. package/runtime/prompts/profiles/implementation-option-selection.md +9 -5
  33. package/runtime/prompts/profiles/implementation-planning.md +13 -7
  34. package/runtime/prompts/profiles/implementation.md +9 -5
  35. package/runtime/prompts/profiles/improvement-discovery.md +10 -6
  36. package/runtime/prompts/profiles/project-analysis.md +9 -5
  37. package/runtime/prompts/profiles/requirements-discovery.md +15 -9
  38. package/runtime/prompts/wizard/prompts.ko.json +18 -1
  39. package/runtime/python/okstra_ctl/adapters/runtime/__init__.py +1 -0
  40. package/runtime/python/okstra_ctl/adapters/runtime/assembly.py +27 -0
  41. package/runtime/python/okstra_ctl/adapters/runtime/cli_wrapper.py +49 -0
  42. package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +74 -0
  43. package/runtime/python/okstra_ctl/application/open_worker.py +29 -0
  44. package/runtime/python/okstra_ctl/assignment_resolver.py +27 -27
  45. package/runtime/python/okstra_ctl/dispatch_core.py +120 -100
  46. package/runtime/python/okstra_ctl/domain/host.py +3 -1
  47. package/runtime/python/okstra_ctl/domain/wizard/interaction.py +5 -0
  48. package/runtime/python/okstra_ctl/domain/worker_runtime.py +44 -0
  49. package/runtime/python/okstra_ctl/implementation_outcome.py +21 -5
  50. package/runtime/python/okstra_ctl/legacy_model_selection.py +115 -27
  51. package/runtime/python/okstra_ctl/manager_sync.py +4 -1
  52. package/runtime/python/okstra_ctl/next_phase.py +236 -0
  53. package/runtime/python/okstra_ctl/ports/worker_runtime.py +26 -0
  54. package/runtime/python/okstra_ctl/recap.py +4 -1
  55. package/runtime/python/okstra_ctl/render.py +46 -47
  56. package/runtime/python/okstra_ctl/role_requirements.py +28 -35
  57. package/runtime/python/okstra_ctl/rollup.py +4 -1
  58. package/runtime/python/okstra_ctl/run.py +8 -5
  59. package/runtime/python/okstra_ctl/stage_fix_carry.py +17 -1
  60. package/runtime/python/okstra_ctl/team.py +14 -18
  61. package/runtime/python/okstra_ctl/wizard.py +248 -56
  62. package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -2
  63. package/runtime/python/okstra_ctl/worker_prompt_contract.py +40 -9
  64. package/runtime/python/okstra_ctl/worker_prompt_headers.py +16 -1
  65. package/runtime/python/okstra_ctl/workflow.py +18 -32
  66. package/runtime/python/okstra_ctl/worktree.py +3 -3
  67. package/runtime/python/okstra_ctl/worktree_registry.py +5 -4
  68. package/runtime/python/okstra_project/state.py +54 -6
  69. package/runtime/schemas/final-report-v2.0.schema.json +43 -5
  70. package/runtime/skills/okstra-inspect/facets/recap.md +2 -0
  71. package/runtime/skills/okstra-inspect/facets/report.md +1 -1
  72. package/runtime/skills/okstra-inspect/facets/status.md +15 -13
  73. package/runtime/skills/okstra-rollup/SKILL.md +1 -0
  74. package/runtime/skills/okstra-run/SKILL.md +5 -5
  75. package/runtime/templates/implementation-worker-preamble.md +3 -18
  76. package/runtime/templates/project-docs/task-index.template.md +0 -1
  77. package/runtime/templates/reports/html/macros/forms.html +5 -4
  78. package/runtime/templates/reports/html/tasks/final-verification.template.html +1 -1
  79. package/runtime/templates/reports/html/tasks/implementation.template.html +1 -1
  80. package/runtime/templates/worker-prompt-preamble.md +3 -36
  81. package/runtime/validators/validate-run.py +57 -99
@@ -73,8 +73,8 @@ from okstra_ctl.incremental_scope import ( # noqa: E402
73
73
  coverage_row_blocked_on,
74
74
  stages_for_clarification,
75
75
  )
76
+ from okstra_ctl import next_phase # noqa: E402
76
77
  from okstra_ctl.workflow import ( # noqa: E402
77
- DEFAULT_NEXT_PHASE,
78
78
  ERROR_ANALYSIS_ROUTING_DIRECTIONS,
79
79
  PHASE_SEQUENCE,
80
80
  REQUIREMENTS_DISCOVERY_ROUTING_TARGETS,
@@ -627,68 +627,6 @@ def write_json(path: Path, payload: dict) -> None:
627
627
  path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n")
628
628
 
629
629
 
630
- def default_next_phase(task_type: str) -> str:
631
- return DEFAULT_NEXT_PHASE.get(task_type, "unknown")
632
-
633
-
634
- def advance_next_phase(
635
- current_phase: str,
636
- phase_sequence: list,
637
- ) -> str:
638
- """Compute the next recommended phase after `current_phase` completes.
639
-
640
- Walk `phase_sequence` and return the entry following `current_phase`.
641
- Falls back to the static `default_next_phase` mapping when the current
642
- phase is the terminal entry or is absent from the sequence (e.g. custom
643
- task types that aren't part of the standard 5-phase lifecycle).
644
- """
645
- if isinstance(phase_sequence, list) and current_phase in phase_sequence:
646
- idx = phase_sequence.index(current_phase)
647
- if idx + 1 < len(phase_sequence):
648
- return phase_sequence[idx + 1]
649
- return "done-or-follow-up"
650
- return default_next_phase(current_phase)
651
-
652
-
653
- def _error_analysis_next_phase(data: Mapping[str, Any]) -> str | None:
654
- if not isinstance(data, Mapping):
655
- return None
656
- header = data.get("header")
657
- if not isinstance(header, Mapping) or header.get("taskType") != "error-analysis":
658
- return None
659
- error_analysis = data.get("errorAnalysis")
660
- if not isinstance(error_analysis, Mapping):
661
- return None
662
- routing = error_analysis.get("routing")
663
- if not isinstance(routing, Mapping):
664
- return None
665
- target = routing.get("nextTaskType")
666
- if target in ERROR_ANALYSIS_ROUTING_DIRECTIONS:
667
- return str(target)
668
- return None
669
-
670
-
671
- def _requirements_discovery_next_phase(data: Mapping[str, Any]) -> str | None:
672
- if not isinstance(data, Mapping):
673
- return None
674
- header = data.get("header")
675
- if (
676
- not isinstance(header, Mapping)
677
- or header.get("taskType") != "requirements-discovery"
678
- ):
679
- return None
680
- requirements = data.get("requirementsDiscovery")
681
- if not isinstance(requirements, Mapping):
682
- return None
683
- routing = requirements.get("routing")
684
- if not isinstance(routing, Mapping):
685
- return None
686
- target = routing.get("nextTaskType")
687
- if target in REQUIREMENTS_DISCOVERY_ROUTING_TARGETS:
688
- return str(target)
689
- return None
690
-
691
-
692
630
  def update_workflow_metadata(
693
631
  run_manifest: dict,
694
632
  task_manifest: dict,
@@ -715,36 +653,49 @@ def update_workflow_metadata(
715
653
  for phase in phase_sequence:
716
654
  phase_states.setdefault(phase, "not-started")
717
655
 
656
+ # 포인터는 리드가 리포트에 저작하고, 이 검증기는 그것을 계산하지 않는다.
657
+ # 여기서 하는 일은 리포트의 Phase 라우팅 투영과 대조하는 것뿐이다.
658
+ authored = next_phase.promote(workflow.get("nextRecommendedPhase"))
659
+ workflow.pop("nextRecommendedPhaseCorrection", None)
660
+
718
661
  if validation_status == "passed":
719
662
  current_phase_state = "completed"
720
663
  if current_phase:
721
664
  phase_states[current_phase] = current_phase_state
722
665
  last_completed_phase = current_phase or workflow.get("lastCompletedPhase", "")
723
- # Validation just passed → actively advance to the next phase in
724
- # the sequence rather than preserving a stale value that may equal
725
- # current_phase (which would cause the lifecycle pointer to stall).
726
- if current_phase == "requirements-discovery":
727
- report_next_phase = _requirements_discovery_next_phase(report_data or {})
728
- elif current_phase == "error-analysis":
729
- report_next_phase = _error_analysis_next_phase(report_data or {})
666
+ # 어긋나면 투영값으로 정정하되 실행은 죽이지 않는다 — 분석이 통과한 run 을
667
+ # 장부 필드 하나 때문에 버리지 않는다 (ADR-0004).
668
+ projected = next_phase.project(report_data or {})
669
+ if (
670
+ authored["phase"] == projected["phase"]
671
+ and authored["status"] == projected["status"]
672
+ ):
673
+ next_recommended_phase = authored
730
674
  else:
731
- report_next_phase = None
732
- next_recommended_phase = report_next_phase or advance_next_phase(
733
- current_phase, phase_sequence
734
- )
675
+ # 리드의 근거는 리드가 고른 phase 를 설명하는 문장이다. phase 를 정정한
676
+ # 뒤에도 들고 오면 포인터가 자기 목적지를 설명하지 않는 문장을 달게 된다.
677
+ # 원문은 아래 correction["authored"] 에 그대로 보존된다.
678
+ next_recommended_phase = next_phase.make(
679
+ phase=projected["phase"],
680
+ status=projected["status"],
681
+ rationale=(
682
+ "리포트 라우팅에서 투영됨. 리드가 쓴 값과 근거는 "
683
+ "nextRecommendedPhaseCorrection.authored 에 있다."
684
+ ),
685
+ )
686
+ # applied 는 사본이다. 살아 있는 포인터와 같은 dict 를 가리키면
687
+ # 나중에 포인터를 제자리 변형하는 호출부가 감사 기록까지 바꾼다.
688
+ workflow["nextRecommendedPhaseCorrection"] = {
689
+ "authored": authored,
690
+ "applied": dict(next_recommended_phase),
691
+ }
735
692
  else:
736
693
  current_phase_state = "blocked"
737
694
  if current_phase:
738
695
  phase_states[current_phase] = current_phase_state
739
696
  last_completed_phase = workflow.get("lastCompletedPhase", "")
740
- next_recommended_phase = current_phase or workflow.get(
741
- "nextRecommendedPhase", ""
742
- )
743
-
744
- routing_status = workflow.get("routingStatus")
745
- if not isinstance(routing_status, str) or not routing_status.strip():
746
- routing_status = (
747
- "pending" if current_phase == "requirements-discovery" else "not-applicable"
697
+ next_recommended_phase = next_phase.make(
698
+ status=next_phase.STATUS_BLOCKED, rationale=authored["rationale"]
748
699
  )
749
700
 
750
701
  awaiting_approval = workflow.get("awaitingApproval")
@@ -792,6 +743,9 @@ def update_workflow_metadata(
792
743
  }
793
744
  )
794
745
 
746
+ # routingStatus 는 포인터 status 로 흡수됐다. update() 는 키를 더할 뿐이라
747
+ # 이전 run 이 남긴 값은 명시적으로 지워야 사라진다.
748
+ workflow.pop("routingStatus", None)
795
749
  workflow.update(
796
750
  {
797
751
  "phaseSequence": phase_sequence,
@@ -801,7 +755,6 @@ def update_workflow_metadata(
801
755
  "lastCompletedPhase": last_completed_phase,
802
756
  "nextRecommendedPhase": next_recommended_phase,
803
757
  "awaitingApproval": awaiting_approval,
804
- "routingStatus": routing_status,
805
758
  "lastSafeCheckpoint": last_safe_checkpoint,
806
759
  }
807
760
  )
@@ -810,6 +763,10 @@ def update_workflow_metadata(
810
763
  workflow_snapshot = run_manifest.get("workflowSnapshot", {})
811
764
  if not isinstance(workflow_snapshot, dict):
812
765
  workflow_snapshot = {}
766
+ # task manifest 의 workflow 는 다음 run 이 덮어쓰므로 정정 기록이 남지 않는다.
767
+ # run 단위 감사 기록은 이 스냅샷이 유일한 보관처다.
768
+ workflow_snapshot.pop("routingStatus", None)
769
+ workflow_snapshot.pop("nextRecommendedPhaseCorrection", None)
813
770
  workflow_snapshot.update(
814
771
  {
815
772
  "phaseSequence": workflow["phaseSequence"],
@@ -817,12 +774,20 @@ def update_workflow_metadata(
817
774
  "currentPhaseState": workflow["currentPhaseState"],
818
775
  "phaseStates": workflow["phaseStates"],
819
776
  "lastCompletedPhase": workflow["lastCompletedPhase"],
820
- "nextRecommendedPhase": workflow["nextRecommendedPhase"],
777
+ # 포인터는 이제 문자열이 아니라 dict 다. 두 매니페스트가 같은 객체를
778
+ # 공유하면 한쪽을 제자리 변형할 때 다른 쪽이 조용히 따라 바뀐다.
779
+ "nextRecommendedPhase": dict(workflow["nextRecommendedPhase"]),
821
780
  "awaitingApproval": workflow["awaitingApproval"],
822
- "routingStatus": workflow["routingStatus"],
823
781
  "lastSafeCheckpoint": workflow["lastSafeCheckpoint"],
824
782
  }
825
783
  )
784
+ if "nextRecommendedPhaseCorrection" in workflow:
785
+ # 두 매니페스트는 별개 파일로 나간다 — dict 를 공유시키지 않는다.
786
+ # 겉 dict 만 복사하면 authored·applied 는 여전히 같은 객체다.
787
+ workflow_snapshot["nextRecommendedPhaseCorrection"] = {
788
+ key: dict(value)
789
+ for key, value in workflow["nextRecommendedPhaseCorrection"].items()
790
+ }
826
791
  run_manifest["workflowSnapshot"] = workflow_snapshot
827
792
 
828
793
 
@@ -7949,13 +7914,6 @@ def _validate_stage_has_requirement(data: dict, failures: list[str]) -> None:
7949
7914
  )
7950
7915
 
7951
7916
 
7952
- _FINAL_VERIFICATION_ROUTING_TOKEN_RE = re.compile(
7953
- r"(?<![A-Za-z-])(?:release-handoff\(stage-group\)|release-handoff|done|"
7954
- r"implementation|error-analysis|implementation-option-selection|"
7955
- r"implementation-planning)(?![A-Za-z-])"
7956
- )
7957
-
7958
-
7959
7917
  def _validate_final_verification_consistency(data: dict, failures: list[str]) -> None:
7960
7918
  """Enforce verdict ↔ blocker/condition/routing consistency on the
7961
7919
  final-verification data.json (SSOT). The schema guarantees field SHAPE;
@@ -7971,15 +7929,15 @@ def _validate_final_verification_consistency(data: dict, failures: list[str]) ->
7971
7929
  blockers = fv.get("acceptanceBlockers") or []
7972
7930
  conditions = verdict.get("conditionalAcceptanceConditions") or []
7973
7931
  routing_value = fv.get("routingRecommendation")
7974
- routing = routing_value if isinstance(routing_value, str) else ""
7975
- routing_tokens = _FINAL_VERIFICATION_ROUTING_TOKEN_RE.findall(routing)
7976
- routing_token = routing_tokens[0] if len(routing_tokens) == 1 else None
7977
-
7978
- if routing_token is None:
7932
+ routing_token = ""
7933
+ if isinstance(routing_value, dict):
7934
+ routing_token = str(routing_value.get("target") or "")
7935
+ if not routing_token:
7979
7936
  failures.append(
7980
- "final-verification: routingRecommendation must contain exactly one "
7981
- "supported routing token."
7937
+ "final-verification: routingRecommendation.target must name exactly one "
7938
+ "supported routing target."
7982
7939
  )
7940
+ routing_token = None
7983
7941
 
7984
7942
  if token == "accepted" and blockers:
7985
7943
  failures.append(