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.
- package/docs/architecture/storage-model.md +2 -2
- package/docs/architecture.md +22 -19
- package/docs/cli.md +18 -14
- package/docs/for-ai/skills/okstra-inspect.md +2 -3
- package/docs/for-ai/skills/okstra-rollup.md +1 -0
- package/docs/project-structure-overview.md +57 -56
- package/docs/task-process/README.md +11 -9
- package/docs/task-process/common-flow.md +13 -16
- package/docs/task-process/error-analysis.md +9 -10
- package/docs/task-process/final-verification.md +7 -7
- package/docs/task-process/implementation-planning.md +9 -9
- package/docs/task-process/implementation.md +6 -6
- package/docs/task-process/release-handoff.md +8 -7
- package/docs/task-process/requirements-discovery.md +8 -8
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/interactive.sh +12 -6
- package/runtime/bin/lib/okstra/usage.sh +3 -2
- package/runtime/bin/okstra-spawn-followups.py +4 -2
- package/runtime/prompts/launch.template.md +2 -2
- package/runtime/prompts/lead/context-loader.md +1 -2
- package/runtime/prompts/lead/okstra-lead-contract.md +3 -4
- package/runtime/prompts/lead/report-writer.md +15 -1
- package/runtime/prompts/lead/team-contract.md +16 -12
- package/runtime/prompts/profiles/_implementation-deliverable.md +1 -1
- package/runtime/prompts/profiles/_implementation-executor.md +0 -3
- package/runtime/prompts/profiles/_implementation-verifier.md +3 -7
- package/runtime/prompts/profiles/change-impact-analysis.md +9 -5
- package/runtime/prompts/profiles/error-analysis.md +14 -8
- package/runtime/prompts/profiles/feature-analysis.md +9 -5
- package/runtime/prompts/profiles/final-verification.md +10 -7
- package/runtime/prompts/profiles/implementation-option-selection.md +9 -5
- package/runtime/prompts/profiles/implementation-planning.md +13 -7
- package/runtime/prompts/profiles/implementation.md +9 -5
- package/runtime/prompts/profiles/improvement-discovery.md +10 -6
- package/runtime/prompts/profiles/project-analysis.md +9 -5
- package/runtime/prompts/profiles/requirements-discovery.md +15 -9
- package/runtime/prompts/wizard/prompts.ko.json +18 -1
- package/runtime/python/okstra_ctl/adapters/runtime/__init__.py +1 -0
- package/runtime/python/okstra_ctl/adapters/runtime/assembly.py +27 -0
- package/runtime/python/okstra_ctl/adapters/runtime/cli_wrapper.py +49 -0
- package/runtime/python/okstra_ctl/adapters/runtime/cmux.py +74 -0
- package/runtime/python/okstra_ctl/application/open_worker.py +29 -0
- package/runtime/python/okstra_ctl/assignment_resolver.py +27 -27
- package/runtime/python/okstra_ctl/dispatch_core.py +120 -100
- package/runtime/python/okstra_ctl/domain/host.py +3 -1
- package/runtime/python/okstra_ctl/domain/wizard/interaction.py +5 -0
- package/runtime/python/okstra_ctl/domain/worker_runtime.py +44 -0
- package/runtime/python/okstra_ctl/implementation_outcome.py +21 -5
- package/runtime/python/okstra_ctl/legacy_model_selection.py +115 -27
- package/runtime/python/okstra_ctl/manager_sync.py +4 -1
- package/runtime/python/okstra_ctl/next_phase.py +236 -0
- package/runtime/python/okstra_ctl/ports/worker_runtime.py +26 -0
- package/runtime/python/okstra_ctl/recap.py +4 -1
- package/runtime/python/okstra_ctl/render.py +46 -47
- package/runtime/python/okstra_ctl/role_requirements.py +28 -35
- package/runtime/python/okstra_ctl/rollup.py +4 -1
- package/runtime/python/okstra_ctl/run.py +8 -5
- package/runtime/python/okstra_ctl/stage_fix_carry.py +17 -1
- package/runtime/python/okstra_ctl/team.py +14 -18
- package/runtime/python/okstra_ctl/wizard.py +248 -56
- package/runtime/python/okstra_ctl/worker_prompt_body.py +18 -2
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +40 -9
- package/runtime/python/okstra_ctl/worker_prompt_headers.py +16 -1
- package/runtime/python/okstra_ctl/workflow.py +18 -32
- package/runtime/python/okstra_ctl/worktree.py +3 -3
- package/runtime/python/okstra_ctl/worktree_registry.py +5 -4
- package/runtime/python/okstra_project/state.py +54 -6
- package/runtime/schemas/final-report-v2.0.schema.json +43 -5
- package/runtime/skills/okstra-inspect/facets/recap.md +2 -0
- package/runtime/skills/okstra-inspect/facets/report.md +1 -1
- package/runtime/skills/okstra-inspect/facets/status.md +15 -13
- package/runtime/skills/okstra-rollup/SKILL.md +1 -0
- package/runtime/skills/okstra-run/SKILL.md +5 -5
- package/runtime/templates/implementation-worker-preamble.md +3 -18
- package/runtime/templates/project-docs/task-index.template.md +0 -1
- package/runtime/templates/reports/html/macros/forms.html +5 -4
- package/runtime/templates/reports/html/tasks/final-verification.template.html +1 -1
- package/runtime/templates/reports/html/tasks/implementation.template.html +1 -1
- package/runtime/templates/worker-prompt-preamble.md +3 -36
- 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
|
-
#
|
|
724
|
-
#
|
|
725
|
-
|
|
726
|
-
if
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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 =
|
|
741
|
-
|
|
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
|
-
|
|
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
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
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
|
|
7981
|
-
"supported routing
|
|
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(
|