okstra 0.186.4 → 0.186.6

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 (44) hide show
  1. package/docs/architecture.md +1 -1
  2. package/docs/cli.md +3 -3
  3. package/docs/for-ai/skills/okstra-user-response.md +1 -1
  4. package/package.json +1 -1
  5. package/runtime/BUILD.json +2 -2
  6. package/runtime/bin/okstra-render-report-views.py +6 -5
  7. package/runtime/prompts/launch.template.md +14 -0
  8. package/runtime/prompts/lead/convergence.md +2 -2
  9. package/runtime/prompts/lead/okstra-lead-contract.md +4 -14
  10. package/runtime/prompts/lead/plan-body-verification.md +4 -3
  11. package/runtime/prompts/lead/report-writer.md +3 -1
  12. package/runtime/prompts/profiles/_coverage-critic.md +1 -1
  13. package/runtime/prompts/profiles/error-analysis.md +2 -2
  14. package/runtime/prompts/profiles/final-verification.md +2 -2
  15. package/runtime/prompts/profiles/implementation-planning.md +3 -3
  16. package/runtime/prompts/profiles/requirements-discovery.md +2 -2
  17. package/runtime/prompts/wizard/prompts.ko.json +2 -4
  18. package/runtime/python/okstra_ctl/adapters/hosts/grok/adapter.py +2 -5
  19. package/runtime/python/okstra_ctl/agent_activity.py +6 -0
  20. package/runtime/python/okstra_ctl/clarification_items.py +67 -11
  21. package/runtime/python/okstra_ctl/next_phase.py +6 -3
  22. package/runtime/python/okstra_ctl/plan_items.py +32 -6
  23. package/runtime/python/okstra_ctl/plan_items_cli.py +57 -3
  24. package/runtime/python/okstra_ctl/render_final_report.py +3 -1
  25. package/runtime/python/okstra_ctl/report_assembly.py +65 -3
  26. package/runtime/python/okstra_ctl/report_html/run_usage.py +5 -1
  27. package/runtime/python/okstra_ctl/report_projections.py +45 -4
  28. package/runtime/python/okstra_ctl/run.py +21 -6
  29. package/runtime/python/okstra_ctl/usage_cells.py +15 -0
  30. package/runtime/python/okstra_ctl/user_response.py +72 -6
  31. package/runtime/python/okstra_ctl/wizard.py +1 -5
  32. package/runtime/python/okstra_token_usage/codex.py +32 -3
  33. package/runtime/python/okstra_token_usage/collect.py +148 -15
  34. package/runtime/python/okstra_token_usage/grok.py +24 -5
  35. package/runtime/python/okstra_token_usage/report.py +12 -2
  36. package/runtime/skills/okstra-user-response/SKILL.md +4 -2
  37. package/runtime/templates/reports/html/assets/base.css +3 -9
  38. package/runtime/templates/reports/html/assets/base.js +0 -21
  39. package/runtime/templates/reports/html/base.template.html +1 -4
  40. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +9 -28
  41. package/runtime/validators/lib/runners.sh +5 -1
  42. package/runtime/validators/validate-report-views.py +2 -1
  43. package/runtime/validators/validate-run.py +240 -102
  44. package/runtime/validators/validate_session_conformance.py +71 -18
@@ -47,6 +47,7 @@ from okstra_ctl.lead_events import ( # noqa: E402
47
47
  )
48
48
  from okstra_ctl.agent_activity import ACTIVITY_FIELDS # noqa: E402
49
49
  from okstra_ctl.domain.host import HostNotRegistered # noqa: E402
50
+ from okstra_ctl.final_report_paths import final_report_data_path # noqa: E402
50
51
  from okstra_ctl.registry.host_registry import default_host_registry # noqa: E402
51
52
  from okstra_ctl.wrapper_status import read_wrapper_status # noqa: E402
52
53
 
@@ -624,16 +625,7 @@ def _plan_body_state_path(run_dir: Path, suffix: str) -> Path:
624
625
 
625
626
  def _ids_reported_as_asked(report_path: Path) -> list[str]:
626
627
  """리포트가 "사용자에게 물었다"고 기록한 열린 승인 차단 행의 id."""
627
- name = report_path.name
628
- if not name.endswith(".md"):
629
- return []
630
- data_path = report_path.with_name(name.removesuffix(".md") + ".data.json")
631
- try:
632
- doc = json.loads(data_path.read_text())
633
- except (OSError, json.JSONDecodeError):
634
- return []
635
- if not isinstance(doc, dict):
636
- return []
628
+ doc = _read_report_data(report_path)
637
629
  asked: list[str] = []
638
630
  for row in doc.get("clarificationItems") or []:
639
631
  if not isinstance(row, dict):
@@ -666,12 +658,13 @@ def _activity_index(events: list[LeadEvent]) -> dict[str, list[LeadEvent]]:
666
658
 
667
659
 
668
660
  def _read_report_data(report_path: Path) -> Mapping[str, Any]:
669
- name = report_path.name
670
- if not name.endswith(".md"):
671
- return {}
672
- data_path = report_path.with_name(name.removesuffix(".md") + ".data.json")
661
+ """`--report` 가 data.json · markdown · html 이어도 같은 레코드를 연다.
662
+
663
+ Phase 7 는 data.json 을 넘긴다. `.md` 만 받던 동안 투영과 self-fix 횟수가
664
+ 빈 객체에서 나와 `projected=<missing>` / `selfFixRoundsApplied=0` 이 됐다.
665
+ """
673
666
  try:
674
- data = json.loads(data_path.read_text(encoding="utf-8"))
667
+ data = json.loads(final_report_data_path(report_path).read_text(encoding="utf-8"))
675
668
  except (OSError, json.JSONDecodeError):
676
669
  return {}
677
670
  return data if isinstance(data, Mapping) else {}
@@ -758,7 +751,16 @@ def _check_projected_agent_activity(
758
751
  {field: event.details.get(field) for field in ACTIVITY_FIELDS}
759
752
  for event in events
760
753
  ]
761
- projected = report_data.get("agentActivity")
754
+ raw_projected = report_data.get("agentActivity")
755
+ projected = (
756
+ [
757
+ {field: row.get(field) for field in ACTIVITY_FIELDS}
758
+ for row in raw_projected
759
+ if isinstance(row, Mapping)
760
+ ]
761
+ if isinstance(raw_projected, list)
762
+ else raw_projected
763
+ )
762
764
  if projected == expected:
763
765
  return
764
766
  mismatch = "length"
@@ -850,6 +852,43 @@ def _allowed_automatic_rounds(self_fix_rounds: int, *, gating: bool = True) -> i
850
852
  return 2 + max(self_fix_rounds, 0)
851
853
 
852
854
 
855
+ def _self_fix_rounds_from_state(run_dir: Path, suffix: str | None) -> int | None:
856
+ """이번 런 상태 파일의 자가수정 횟수. 없으면 None — 호출자가 리포트로 폴백.
857
+
858
+ 리포트 칸은 이어진 seq 의 누적이라, 이번 창의 `self-fix-applied` 건수와
859
+ 비교하면 어긋난다.
860
+ """
861
+ if not suffix:
862
+ return None
863
+ try:
864
+ doc = json.loads(_plan_body_state_path(run_dir, suffix).read_text())
865
+ except (OSError, json.JSONDecodeError):
866
+ return None
867
+ if not isinstance(doc, dict):
868
+ return None
869
+ projection = doc.get("planBodyVerification")
870
+ value = (
871
+ projection.get("selfFixRoundsApplied")
872
+ if isinstance(projection, dict)
873
+ else None
874
+ )
875
+ if isinstance(value, int) and value >= 0:
876
+ return value
877
+ value = doc.get("selfFixRoundsApplied")
878
+ return value if isinstance(value, int) and value >= 0 else None
879
+
880
+
881
+ def _is_plan_body_verification_activity(event: LeadEvent) -> bool:
882
+ """`verification-round-completed` 가 계획 본문 배치인지.
883
+
884
+ 같은 kind 로 적대 재검증 라운드도 남는다. 그 건을 본문 라운드에 넣으면
885
+ recorded 가 roundCount 보다 커진다. 요약이 `adversarial reverify` 이면
886
+ 본문이 아니다. 픽스처 요약(`verification-round-completed for …`)은 본문이다.
887
+ """
888
+ summary = str(event.details.get("summary") or "").lower()
889
+ return "adversarial reverify" not in summary and "adversarial re-verify" not in summary
890
+
891
+
853
892
  def _plan_body_verification(report_data: Mapping[str, Any]) -> Mapping[str, Any] | None:
854
893
  planning = report_data.get("implementationPlanning")
855
894
  verification = (
@@ -970,14 +1009,23 @@ def _check_activity_round_counts(
970
1009
  errors: list[str],
971
1010
  ) -> None:
972
1011
  verification_rounds = _plan_body_rounds_ran(run_dir, suffix)
973
- recorded_verifications = len(indexed.get("verification-round-completed", []))
1012
+ recorded_verifications = len([
1013
+ event
1014
+ for event in indexed.get("verification-round-completed", [])
1015
+ if _is_plan_body_verification_activity(event)
1016
+ ])
974
1017
  user_reverification_rounds = _resolved_correctness_reverification_rounds(
975
1018
  indexed,
976
1019
  report_data,
977
1020
  verification_rounds,
978
1021
  )
979
1022
  automatic_rounds = verification_rounds - len(user_reverification_rounds)
980
- self_fix_rounds = _self_fix_rounds_applied(report_data)
1023
+ state_self_fix = _self_fix_rounds_from_state(run_dir, suffix)
1024
+ self_fix_rounds = (
1025
+ state_self_fix
1026
+ if state_self_fix is not None
1027
+ else _self_fix_rounds_applied(report_data)
1028
+ )
981
1029
  allowed_rounds = _allowed_automatic_rounds(
982
1030
  self_fix_rounds, gating=_plan_body_gating(report_data),
983
1031
  )
@@ -1435,6 +1483,11 @@ def _check_cmux_adapter_read(
1435
1483
  return
1436
1484
  if evidence.sidecar_reads.get(CMUX_ADAPTER_BASENAME):
1437
1485
  return
1486
+ dispatch_mode = str(team_state.get("dispatchMode", "")).strip()
1487
+ if dispatch_mode in _WORKER_DISPATCH_MODES:
1488
+ # grok 같은 artifact-only 호스트는 Read 도구 기록이 없다. 워커가
1489
+ # okstra 디스패치로 나갔으면 어댑터가 가리키는 경로를 탄 것이다.
1490
+ return
1438
1491
  errors.append(
1439
1492
  f"cmux adapter: no read of `{CMUX_ADAPTER_BASENAME}` (a `Read` call or a "
1440
1493
  f"shell command naming it) found in the "