okstra 0.191.2 → 0.193.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 (37) hide show
  1. package/docs/architecture.md +2 -2
  2. package/docs/cli.md +3 -2
  3. package/docs/project-structure-overview.md +3 -1
  4. package/docs/task-process/README.md +2 -2
  5. package/docs/task-process/common-flow.md +4 -5
  6. package/package.json +1 -1
  7. package/runtime/BUILD.json +2 -2
  8. package/runtime/agents/workers/translator-worker.md +1 -1
  9. package/runtime/prompts/launch.template.md +1 -1
  10. package/runtime/prompts/lead/convergence.md +13 -3
  11. package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
  12. package/runtime/prompts/lead/plan-body-verification.md +1 -1
  13. package/runtime/prompts/lead/report-writer.md +11 -8
  14. package/runtime/prompts/profiles/_implementation-executor.md +1 -1
  15. package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
  16. package/runtime/prompts/profiles/implementation-planning.md +1 -1
  17. package/runtime/prompts/wizard/prompts.ko.json +52 -23
  18. package/runtime/python/okstra_ctl/conformance.py +74 -0
  19. package/runtime/python/okstra_ctl/convergence.py +63 -1
  20. package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +238 -0
  21. package/runtime/python/okstra_ctl/dispatch_core.py +42 -22
  22. package/runtime/python/okstra_ctl/execution_mutation_audit.py +96 -8
  23. package/runtime/python/okstra_ctl/next_phase.py +18 -8
  24. package/runtime/python/okstra_ctl/plan_items.py +6 -4
  25. package/runtime/python/okstra_ctl/plan_items_cli.py +91 -6
  26. package/runtime/python/okstra_ctl/report_finalize.py +57 -10
  27. package/runtime/python/okstra_ctl/report_translation_dispatch.py +300 -0
  28. package/runtime/python/okstra_ctl/verdict_blocks.py +37 -7
  29. package/runtime/python/okstra_ctl/wizard/engine.py +16 -2
  30. package/runtime/python/okstra_ctl/wizard/registry.py +11 -2
  31. package/runtime/python/okstra_ctl/wizard/roles.py +364 -361
  32. package/runtime/python/okstra_ctl/wizard/state.py +39 -27
  33. package/runtime/python/okstra_ctl/wizard/steps_identity.py +50 -8
  34. package/runtime/python/okstra_ctl/wizard/steps_roles.py +1 -0
  35. package/runtime/python/okstra_ctl/worker_prompt_contract.py +11 -0
  36. package/runtime/skills/okstra-run/SKILL.md +2 -2
  37. package/runtime/validators/validate-run.py +78 -16
@@ -5,6 +5,14 @@ The response shape is fixed by contract (`prompts/lead/plan-body-verification.md
5
5
  per-round ad-hoc regex makes the round's fidelity depend on whoever wrote it,
6
6
  and its failure mode is silence: dev-10400 lost 19 of 37 assigned items to a
7
7
  no-match that nothing reported. Every shape this module cannot read is an error.
8
+
9
+ A label that means the same thing is read, not refused. The contract writes
10
+ `**Verdict**: AGREE`; a worker following a lead-authored instruction wrote
11
+ `- Verdict: REFUTED` and the whole reverify round was rejected for it
12
+ (2026-09-09). The bullet form, the colon inside the bold (`**Verdict:**`), a
13
+ bullet before the bold, and a differently-cased key all name the same field —
14
+ refusing them costs a dispatch cycle and yields no information. A bare
15
+ `Verdict:` without bullet or bold is still prose: `Note:` opens sentences.
8
16
  """
9
17
  from __future__ import annotations
10
18
 
@@ -60,11 +68,19 @@ DISAGREE_BASES = frozenset({"counter-evidence", "burden-not-met"})
60
68
  _ITEM_RE = re.compile(r"^###[ \t]+(?P<id>[^\s:]+)[ \t]*:?.*$", re.MULTILINE)
61
69
  # The contract writes some labels with a parenthetical qualifier —
62
70
  # `**Fixability** (only when DISAGREE):` — so the colon may trail a `(...)`.
71
+ # The label shapes read as one field (see the module docstring):
72
+ # `**Key**: v` `**Key:** v` `- Key: v` `- **Key**: v` `- **Key:** v`
73
+ # `_field_match` enforces that an opening `**` is closed exactly once and that
74
+ # a label without bold carries a bullet.
75
+ _FIELD_KEYS = ("Verdict", "Fixability", "Note", "Explanation", "Prior dissent",
76
+ "Basis", "Your evidence")
77
+ _CANONICAL_KEYS = {key.lower(): key for key in _FIELD_KEYS}
63
78
  _FIELD_RE = re.compile(
64
- r"^\*\*(?P<key>Verdict|Fixability|Note|Explanation|Prior dissent|Basis"
65
- r"|Your evidence)\*\*"
66
- r"(?:[ \t]*\([^)]*\))?[ \t]*:[ \t]*(?P<value>.*)$",
67
- re.MULTILINE,
79
+ r"^(?P<bullet>[-*+][ \t]+)?(?P<open>\*\*)?"
80
+ r"(?P<key>" + "|".join(re.escape(key) for key in _FIELD_KEYS) + r")"
81
+ r"(?P<close>\*\*)?(?:[ \t]*\([^)]*\))?[ \t]*:(?P<close_after>\*\*)?"
82
+ r"[ \t]*(?P<value>.*)$",
83
+ re.IGNORECASE,
68
84
  )
69
85
  _VERDICT_RE = re.compile(r"^(?P<token>[A-Z-]+)(?:\((?P<kind>[a-f])\))?$")
70
86
  _PROSE_FIELDS = frozenset({"Explanation", "Note", "Prior dissent", "Your evidence"})
@@ -75,6 +91,20 @@ class VerdictBlockError(ValueError):
75
91
  """Raised when a worker response does not match the contract shape."""
76
92
 
77
93
 
94
+ def _field_match(line: str) -> tuple[str, str] | None:
95
+ """`(canonical key, value)` when *line* is a field label, else ``None``."""
96
+ match = _FIELD_RE.match(line)
97
+ if match is None:
98
+ return None
99
+ opened = bool(match.group("open"))
100
+ closes = sum(1 for name in ("close", "close_after") if match.group(name))
101
+ if opened and closes != 1:
102
+ return None
103
+ if not opened and (closes or not match.group("bullet")):
104
+ return None
105
+ return _CANONICAL_KEYS[match.group("key").lower()], match.group("value")
106
+
107
+
78
108
  @dataclass(frozen=True)
79
109
  class FindingVote:
80
110
  """One worker's vote on one convergence finding, in schema vocabulary."""
@@ -112,12 +142,12 @@ def _scan_blocks(text: str) -> dict[str, dict[str, str]]:
112
142
  continue
113
143
  if not item_id:
114
144
  continue
115
- match = _FIELD_RE.match(line)
145
+ match = _field_match(line)
116
146
  if match:
117
- field = match.group("key")
147
+ field, value = match
118
148
  if field in blocks[item_id]:
119
149
  raise VerdictBlockError(f"item `{item_id}` has duplicate field `{field}`")
120
- blocks[item_id][field] = match.group("value")
150
+ blocks[item_id][field] = value
121
151
  elif field in _PROSE_FIELDS:
122
152
  blocks[item_id][field] += "\n" + line
123
153
  return {
@@ -180,8 +180,13 @@ def _passed_screens(state: WizardState) -> int:
180
180
 
181
181
 
182
182
  def _sim_answer(prompt: Prompt) -> str:
183
- """분모 추정 시뮬레이션의 기본답: pick 은 첫 옵션(추천), text 는 빈 값."""
183
+ """분모 추정 시뮬레이션의 기본답: pick 은 추천(체크박스는 추천 집합, 없으면
184
+ 첫 옵션), text 는 빈 값."""
184
185
  if prompt.kind == "pick" and prompt.options:
186
+ if prompt.multi:
187
+ flagged = [option.value for option in prompt.options if option.recommended]
188
+ if flagged:
189
+ return ",".join(flagged)
185
190
  return prompt.options[0].value
186
191
  return ""
187
192
 
@@ -191,7 +196,10 @@ def _sim_advance(state: WizardState, prompt: Prompt) -> None:
191
196
  _submit_group() 은 호출하지 않고 step.submit 만 직접 호출해 재귀를 막는다."""
192
197
  try:
193
198
  if _is_role_selection_step(prompt.step):
194
- _submit_role_prompt(state, prompt, _sim_answer(prompt))
199
+ # 화면은 호스트 한도에 맞춰 쪽으로 잘린 사본일 수 있다 — 기본답은
200
+ # 잘리지 않은 원본(추천 집합)에서 낸다.
201
+ original = next_role_prompt(state) or prompt
202
+ _submit_role_prompt(state, original, _sim_answer(original))
195
203
  if prompt.step not in state.answered:
196
204
  state.answered.append(prompt.step)
197
205
  if prompt.step not in state.role_selection_order:
@@ -369,6 +377,12 @@ def submit(state: WizardState, value: str) -> dict[str, Any]:
369
377
  if prompt.kind == "pick_group":
370
378
  return _submit_group(state, prompt, value)
371
379
  if _is_role_selection_step(prompt.step):
380
+ # 화면은 호스트 한도에 맞춰 쪽으로 잘린 사본일 수 있다(`present_picker`).
381
+ # 답은 잘리지 않은 원본의 선택지로 검증한다 — 체크박스의 CSV 는 여러
382
+ # 쪽에 걸쳐 고른 값이다.
383
+ original = next_role_prompt(state)
384
+ if original is not None and original.step == prompt.step:
385
+ prompt = original
372
386
  echo = _submit_role_prompt(state, prompt, value or "")
373
387
  if prompt.step not in state.answered:
374
388
  state.answered.append(prompt.step)
@@ -648,11 +648,20 @@ STEPS: list[Step] = [
648
648
  # 이어가기에 필수인 직전 final-report 가 존재하면 use_defaults 와 무관하게
649
649
  # 입력 기회를 노출한다. (과거: use_defaults 게이트 뒤에 숨어 "Use defaults"
650
650
  # 를 고르면 직전 리포트가 통째로 누락됐다.)
651
+ # implementation-planning 의 `--clarification-response` 는 같은 phase 의
652
+ # 계획서만 받는다(`run._validate_planning_entry_inputs`). 그 계획서가 없는
653
+ # 첫 계획 run 에서 이 화면은 `없음 / 직접 입력` 뿐이고, 직접 입력으로 넣을
654
+ # 수 있는 파일도 없다 — 질문이 아니라 막다른 길이다(실측 2026-09-09, grok:
655
+ # 사용자는 직전 후보비교 run 의 user-responses/ 답변을 넘길 자리를 여기서
656
+ # 찾았다). 그 답변은 방향 선택 단계(`selected_direction_pick`)가 리포트와
657
+ # 함께 전달하므로, 계획 run 은 이어받을 계획서가 있을 때만 여기를 묻는다.
651
658
  Step(S_CLARIFICATION_PICK,
652
659
  applies=lambda s: (S_CLARIFICATION_PICK not in s.answered
653
660
  and s.use_defaults is not None
654
- and (s.use_defaults is False
655
- or bool(_suggest_latest_final_report(s)))),
661
+ and (bool(_suggest_latest_final_report(s))
662
+ if s.task_type == "implementation-planning"
663
+ else (s.use_defaults is False
664
+ or bool(_suggest_latest_final_report(s))))),
656
665
  build=_build_clarification_pick, submit=_submit_clarification_pick,
657
666
  owns=("clarification_response_path", "clarification_pending_text")),
658
667
  Step(S_CLARIFICATION,