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.
- package/docs/architecture.md +2 -2
- package/docs/cli.md +3 -2
- package/docs/project-structure-overview.md +3 -1
- package/docs/task-process/README.md +2 -2
- package/docs/task-process/common-flow.md +4 -5
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/agents/workers/translator-worker.md +1 -1
- package/runtime/prompts/launch.template.md +1 -1
- package/runtime/prompts/lead/convergence.md +13 -3
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/lead/plan-body-verification.md +1 -1
- package/runtime/prompts/lead/report-writer.md +11 -8
- package/runtime/prompts/profiles/_implementation-executor.md +1 -1
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/implementation-planning.md +1 -1
- package/runtime/prompts/wizard/prompts.ko.json +52 -23
- package/runtime/python/okstra_ctl/conformance.py +74 -0
- package/runtime/python/okstra_ctl/convergence.py +63 -1
- package/runtime/python/okstra_ctl/convergence_reverify_prompt.py +238 -0
- package/runtime/python/okstra_ctl/dispatch_core.py +42 -22
- package/runtime/python/okstra_ctl/execution_mutation_audit.py +96 -8
- package/runtime/python/okstra_ctl/next_phase.py +18 -8
- package/runtime/python/okstra_ctl/plan_items.py +6 -4
- package/runtime/python/okstra_ctl/plan_items_cli.py +91 -6
- package/runtime/python/okstra_ctl/report_finalize.py +57 -10
- package/runtime/python/okstra_ctl/report_translation_dispatch.py +300 -0
- package/runtime/python/okstra_ctl/verdict_blocks.py +37 -7
- package/runtime/python/okstra_ctl/wizard/engine.py +16 -2
- package/runtime/python/okstra_ctl/wizard/registry.py +11 -2
- package/runtime/python/okstra_ctl/wizard/roles.py +364 -361
- package/runtime/python/okstra_ctl/wizard/state.py +39 -27
- package/runtime/python/okstra_ctl/wizard/steps_identity.py +50 -8
- package/runtime/python/okstra_ctl/wizard/steps_roles.py +1 -0
- package/runtime/python/okstra_ctl/worker_prompt_contract.py +11 -0
- package/runtime/skills/okstra-run/SKILL.md +2 -2
- 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"
|
|
65
|
-
r"|
|
|
66
|
-
r"(?:[ \t]*\([^)]*\))?[ \t]*:
|
|
67
|
-
|
|
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 =
|
|
145
|
+
match = _field_match(line)
|
|
116
146
|
if match:
|
|
117
|
-
field = match
|
|
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] =
|
|
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 은
|
|
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
|
-
|
|
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
|
|
655
|
-
|
|
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,
|