okstra 0.122.0 → 0.123.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/README.md +4 -2
- package/docs/architecture/storage-model.md +14 -0
- package/docs/architecture.md +34 -6
- package/docs/cli.md +45 -5
- package/docs/for-ai/README.md +2 -2
- package/docs/for-ai/skills/okstra-rollup.md +1 -1
- package/docs/for-ai/skills/{okstra-schedule.md → okstra-schedule-gen.md} +3 -3
- package/docs/project-structure-overview.md +3 -2
- package/docs/task-process/implementation-planning.md +33 -9
- package/docs/task-process/implementation.md +21 -2
- package/package.json +1 -1
- package/runtime/BUILD.json +2 -2
- package/runtime/bin/lib/okstra/usage.sh +3 -3
- package/runtime/prompts/launch.template.md +5 -2
- package/runtime/prompts/lead/convergence.md +1 -1
- package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
- package/runtime/prompts/lead/plan-body-verification.md +29 -0
- package/runtime/prompts/lead/report-writer.md +9 -3
- package/runtime/prompts/profiles/_common-contract.md +1 -1
- package/runtime/prompts/profiles/_implementation-deliverable.md +3 -1
- package/runtime/prompts/profiles/_implementation-executor.md +5 -0
- package/runtime/prompts/profiles/_implementation-verifier.md +1 -1
- package/runtime/prompts/profiles/final-verification.md +2 -0
- package/runtime/prompts/profiles/implementation-planning.md +11 -1
- package/runtime/prompts/wizard/prompts.ko.json +44 -0
- package/runtime/python/okstra_ctl/codex_dispatch.py +23 -1
- package/runtime/python/okstra_ctl/design_prep.py +1462 -0
- package/runtime/python/okstra_ctl/design_surfaces.py +243 -0
- package/runtime/python/okstra_ctl/final_report_schema.py +33 -1
- package/runtime/python/okstra_ctl/implementation_stage.py +35 -0
- package/runtime/python/okstra_ctl/incremental_carry.py +294 -21
- package/runtime/python/okstra_ctl/incremental_scope.py +51 -5
- package/runtime/python/okstra_ctl/material.py +1 -1
- package/runtime/python/okstra_ctl/model_discovery.py +98 -0
- package/runtime/python/okstra_ctl/models.py +8 -3
- package/runtime/python/okstra_ctl/render.py +5 -0
- package/runtime/python/okstra_ctl/run.py +53 -5
- package/runtime/python/okstra_ctl/user_response.py +67 -2
- package/runtime/python/okstra_ctl/wizard.py +283 -3
- package/runtime/python/okstra_token_usage/report.py +11 -0
- package/runtime/schemas/final-report-v1.0.schema.json +336 -0
- package/runtime/skills/okstra-inspect/SKILL.md +2 -2
- package/runtime/skills/okstra-rollup/SKILL.md +1 -1
- package/runtime/skills/{okstra-schedule → okstra-schedule-gen}/SKILL.md +2 -2
- package/runtime/skills/okstra-user-response/SKILL.md +8 -6
- package/runtime/templates/reports/final-report.template.md +67 -0
- package/runtime/templates/reports/i18n/en.json +31 -0
- package/runtime/templates/reports/i18n/ko.json +31 -0
- package/runtime/validators/validate-run.py +426 -5
- package/runtime/validators/validate-schedule.py +4 -4
- package/src/cli-registry.mjs +7 -0
- package/src/commands/inspect/design-prep.mjs +23 -0
- package/src/lib/skill-catalog.mjs +2 -1
|
@@ -46,6 +46,7 @@ from .final_report_schema import load_schema
|
|
|
46
46
|
from .final_report_paths import final_report_data_path as _final_report_data_path
|
|
47
47
|
from .lead_events import LeadEvent, append_lead_event
|
|
48
48
|
from .lead_runtime import ALLOWED_LEAD_RUNTIMES
|
|
49
|
+
from .model_discovery import normalize_execution_for_dispatch
|
|
49
50
|
from .models import ModelAssignment, default_model, resolve_model_metadata
|
|
50
51
|
from .schema_excerpt import build_schema_excerpt
|
|
51
52
|
from .path_resolve import relative_to_project_root, resolve_user_file
|
|
@@ -1412,6 +1413,27 @@ def _resolve_model_bindings(inp: PrepareInputs, workers: list[str]) -> _ModelBin
|
|
|
1412
1413
|
)
|
|
1413
1414
|
|
|
1414
1415
|
|
|
1416
|
+
def _normalized_execution(meta: ModelAssignment, provider: str, role: str) -> str:
|
|
1417
|
+
"""Route a role's catalog execution value through the CLI-identity normalizer
|
|
1418
|
+
so the manifest carries the dispatch-correct spelling. SSOT for the mapping
|
|
1419
|
+
lives in model_discovery; this is only the run.py call seam."""
|
|
1420
|
+
return normalize_execution_for_dispatch(
|
|
1421
|
+
provider=provider, execution=meta.execution, display=meta.display, role=role,
|
|
1422
|
+
)
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
def _worker_execution_value(
|
|
1426
|
+
meta: ModelAssignment, provider: str, role: str, workers: list[str],
|
|
1427
|
+
) -> str:
|
|
1428
|
+
"""Normalize a worker role's execution value only when that provider's
|
|
1429
|
+
worker is in the resolved roster; otherwise return the raw catalog value.
|
|
1430
|
+
A worker that will not be dispatched must not probe its CLI or hard-fail
|
|
1431
|
+
the whole prep (the CLI-identity check belongs only to workers that run)."""
|
|
1432
|
+
if provider in workers:
|
|
1433
|
+
return _normalized_execution(meta, provider=provider, role=role)
|
|
1434
|
+
return meta.execution
|
|
1435
|
+
|
|
1436
|
+
|
|
1415
1437
|
StageRunClaim = _implementation_stage.StageRunClaim
|
|
1416
1438
|
|
|
1417
1439
|
|
|
@@ -1577,7 +1599,9 @@ def _write_instruction_set_sources(
|
|
|
1577
1599
|
instruction_set = Path(ctx["INSTRUCTION_SET_PATH"])
|
|
1578
1600
|
instruction_set.mkdir(parents=True, exist_ok=True)
|
|
1579
1601
|
profile_rendered = profile_content
|
|
1580
|
-
|
|
1602
|
+
if inp.task_type == "implementation":
|
|
1603
|
+
profile_rendered += "\n\n{{DESIGN_PREP_CONTEXT}}"
|
|
1604
|
+
profile_tokens = (
|
|
1581
1605
|
"EXECUTOR_PROVIDER",
|
|
1582
1606
|
"EXECUTOR_DISPLAY_NAME",
|
|
1583
1607
|
"EXECUTOR_WORKER_AGENT",
|
|
@@ -1588,10 +1612,28 @@ def _write_instruction_set_sources(
|
|
|
1588
1612
|
"EXECUTOR_WORKTREE_BASE_REF",
|
|
1589
1613
|
"EXECUTOR_WORKTREE_STATUS",
|
|
1590
1614
|
"EXECUTOR_WORKTREE_NOTE",
|
|
1615
|
+
"DESIGN_PREP_CONTEXT",
|
|
1591
1616
|
"PHASE_FORBIDDEN_ACTIONS",
|
|
1592
|
-
)
|
|
1617
|
+
)
|
|
1618
|
+
for key in profile_tokens:
|
|
1593
1619
|
profile_rendered = profile_rendered.replace("{{" + key + "}}", ctx.get(key, ""))
|
|
1594
1620
|
(instruction_set / "analysis-profile.md").write_text(profile_rendered, encoding="utf-8")
|
|
1621
|
+
if inp.task_type == "implementation":
|
|
1622
|
+
executor_source = (
|
|
1623
|
+
Path(ctx["WORKSPACE_ROOT"])
|
|
1624
|
+
/ "prompts"
|
|
1625
|
+
/ "profiles"
|
|
1626
|
+
/ "_implementation-executor.md"
|
|
1627
|
+
)
|
|
1628
|
+
executor_rendered = executor_source.read_text(encoding="utf-8")
|
|
1629
|
+
for key in profile_tokens:
|
|
1630
|
+
executor_rendered = executor_rendered.replace(
|
|
1631
|
+
"{{" + key + "}}", ctx.get(key, "")
|
|
1632
|
+
)
|
|
1633
|
+
(instruction_set / "implementation-executor.md").write_text(
|
|
1634
|
+
executor_rendered,
|
|
1635
|
+
encoding="utf-8",
|
|
1636
|
+
)
|
|
1595
1637
|
(instruction_set / "analysis-material.md").write_text(review_material, encoding="utf-8")
|
|
1596
1638
|
shutil.copyfile(inp.brief_path, instruction_set / "task-brief.md")
|
|
1597
1639
|
if inp.clarification_response_path:
|
|
@@ -2058,16 +2100,22 @@ def prepare_task_bundle(inp: PrepareInputs) -> PrepareOutputs:
|
|
|
2058
2100
|
"CLAUDE_WORKER_MODEL": cw.display,
|
|
2059
2101
|
"CLAUDE_WORKER_MODEL_EXECUTION_VALUE": cw.execution,
|
|
2060
2102
|
"CODEX_WORKER_MODEL": co.display,
|
|
2061
|
-
"CODEX_WORKER_MODEL_EXECUTION_VALUE":
|
|
2103
|
+
"CODEX_WORKER_MODEL_EXECUTION_VALUE": _worker_execution_value(
|
|
2104
|
+
co, "codex", "worker", workers,
|
|
2105
|
+
),
|
|
2062
2106
|
"ANTIGRAVITY_WORKER_MODEL": ge.display,
|
|
2063
|
-
"ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE":
|
|
2107
|
+
"ANTIGRAVITY_WORKER_MODEL_EXECUTION_VALUE": _worker_execution_value(
|
|
2108
|
+
ge, "antigravity", "worker", workers,
|
|
2109
|
+
),
|
|
2064
2110
|
"REPORT_WRITER_MODEL": rw.display,
|
|
2065
2111
|
"REPORT_WRITER_MODEL_EXECUTION_VALUE": rw.execution,
|
|
2066
2112
|
"EXECUTOR_PROVIDER": executor_provider,
|
|
2067
2113
|
"EXECUTOR_DISPLAY_NAME": executor_display_name,
|
|
2068
2114
|
"EXECUTOR_WORKER_AGENT": executor_worker_agent,
|
|
2069
2115
|
"EXECUTOR_MODEL_DISPLAY": executor_model_meta.display,
|
|
2070
|
-
"EXECUTOR_MODEL_EXECUTION_VALUE":
|
|
2116
|
+
"EXECUTOR_MODEL_EXECUTION_VALUE": _normalized_execution(
|
|
2117
|
+
executor_model_meta, provider=executor_provider, role="executor",
|
|
2118
|
+
),
|
|
2071
2119
|
"CRITIC_CHOICE": critic_choice,
|
|
2072
2120
|
"RELATED_TASKS_JSON": related_tasks_json_str,
|
|
2073
2121
|
"RELATED_TASKS_BULLETS": bullets,
|
|
@@ -26,6 +26,7 @@ from okstra_ctl.clarification_items import (
|
|
|
26
26
|
parse_clarification_rows,
|
|
27
27
|
scan_approval_gate,
|
|
28
28
|
section_1_present_but_unparsed,
|
|
29
|
+
_section_1_slice,
|
|
29
30
|
)
|
|
30
31
|
|
|
31
32
|
_APPROVAL_HEADING_RE = re.compile(r"^## APPROVAL\s*$", re.MULTILINE)
|
|
@@ -146,8 +147,14 @@ def list_awaiting_tasks(home: Path, project_id: str, limit: int) -> list[dict]:
|
|
|
146
147
|
return out[:limit] if limit > 0 else out
|
|
147
148
|
|
|
148
149
|
|
|
149
|
-
|
|
150
|
+
# ID tokens are the report body's own cross-reference labels (RB-002, FU-001,
|
|
151
|
+
# O-002, C-017 …) — always uppercase-led so `gpt-5` and lowercase slugs don't
|
|
152
|
+
# match. `§x.y` and `path.ext:line` are the other two ref shapes.
|
|
153
|
+
_SECTION_REF_RE = re.compile(r"§[\d.]+|[A-Z]{1,4}-\d+|[\w./-]+\.\w+:\d+")
|
|
154
|
+
_ID_TOKEN_RE = re.compile(r"^[A-Z]{1,4}-\d+$")
|
|
150
155
|
_ALTERNATIVES_CUE = "Alternatives:"
|
|
156
|
+
_DEFINITION_SNIPPET_CAP = 200
|
|
157
|
+
_SNIPPET_NOISE_RE = re.compile(r'<a id="[^"]*"></a>|`|\*\*')
|
|
151
158
|
|
|
152
159
|
|
|
153
160
|
def _alternatives_from_expected(expected_form: str) -> list[str]:
|
|
@@ -159,6 +166,63 @@ def _alternatives_from_expected(expected_form: str) -> list[str]:
|
|
|
159
166
|
return [p.strip(" .,;—-") for p in parts if p.strip(" .,;—-")]
|
|
160
167
|
|
|
161
168
|
|
|
169
|
+
def _clean_snippet(line: str) -> str:
|
|
170
|
+
s = _SNIPPET_NOISE_RE.sub("", line).strip()
|
|
171
|
+
s = re.sub(r"^[-*+]\s+|^\d+\.\s+", "", s) # drop a leading list marker
|
|
172
|
+
s = re.sub(r"\s*\|\s*", " | ", s).strip(" |")
|
|
173
|
+
s = re.sub(r"\s+", " ", s)
|
|
174
|
+
if len(s) > _DEFINITION_SNIPPET_CAP:
|
|
175
|
+
s = s[:_DEFINITION_SNIPPET_CAP].rstrip() + "…"
|
|
176
|
+
return s
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _resolve_id_token(report_text: str, token: str, s1_slice: str | None) -> str | None:
|
|
180
|
+
"""Find where an internal report token (RB-002, FU-001, …) is defined.
|
|
181
|
+
|
|
182
|
+
Prefer a body line where the token is followed by a `:`/`—`/`-`/`)`
|
|
183
|
+
separator (its definition), else the first body occurrence. §1 rows are
|
|
184
|
+
skipped so the clarification statement being explained is never returned
|
|
185
|
+
as its own definition."""
|
|
186
|
+
definition_re = re.compile(rf"{re.escape(token)}\**\s*[)\]]?\s*[:—–-]")
|
|
187
|
+
fallback: str | None = None
|
|
188
|
+
for line in report_text.splitlines():
|
|
189
|
+
if token not in line:
|
|
190
|
+
continue
|
|
191
|
+
stripped = line.strip()
|
|
192
|
+
if s1_slice is not None and stripped and stripped in s1_slice:
|
|
193
|
+
continue
|
|
194
|
+
if definition_re.search(stripped):
|
|
195
|
+
return _clean_snippet(stripped)
|
|
196
|
+
if fallback is None:
|
|
197
|
+
fallback = _clean_snippet(stripped)
|
|
198
|
+
return fallback
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _resolve_section_ref(report_text: str, ref: str) -> str | None:
|
|
202
|
+
num = ref.lstrip("§")
|
|
203
|
+
heading_re = re.compile(rf"^#{{1,6}}\s+{re.escape(num)}(?=[.\s]).*$", re.MULTILINE)
|
|
204
|
+
m = heading_re.search(report_text)
|
|
205
|
+
return _clean_snippet(m.group(0).lstrip("# ").strip()) if m else None
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def resolve_refs(report_text: str, refs: list[str]) -> list[dict]:
|
|
209
|
+
"""Resolve each context ref to its plain-text definition from the report
|
|
210
|
+
body, or None when the report text alone cannot resolve it (a `path:line`
|
|
211
|
+
pointer). The deterministic lookup the skill uses to rewrite each
|
|
212
|
+
clarification question in self-contained plain language."""
|
|
213
|
+
s1_slice = _section_1_slice(report_text)
|
|
214
|
+
resolved: list[dict] = []
|
|
215
|
+
for ref in refs:
|
|
216
|
+
if ref.startswith("§"):
|
|
217
|
+
definition = _resolve_section_ref(report_text, ref)
|
|
218
|
+
elif _ID_TOKEN_RE.match(ref):
|
|
219
|
+
definition = _resolve_id_token(report_text, ref, s1_slice)
|
|
220
|
+
else:
|
|
221
|
+
definition = None
|
|
222
|
+
resolved.append({"ref": ref, "definition": definition})
|
|
223
|
+
return resolved
|
|
224
|
+
|
|
225
|
+
|
|
162
226
|
def show_open_rows(report_path: Path) -> dict:
|
|
163
227
|
text = report_path.read_text(encoding="utf-8")
|
|
164
228
|
rows = []
|
|
@@ -172,7 +236,8 @@ def show_open_rows(report_path: Path) -> dict:
|
|
|
172
236
|
"status": it.status, "statement": statement,
|
|
173
237
|
"recommended": expected, # raw cell keeps answer + rationale
|
|
174
238
|
"alternatives": _alternatives_from_expected(expected),
|
|
175
|
-
"contextRefs": refs
|
|
239
|
+
"contextRefs": refs,
|
|
240
|
+
"resolvedRefs": resolve_refs(text, refs)})
|
|
176
241
|
return {"reportPath": str(report_path), "rows": rows}
|
|
177
242
|
|
|
178
243
|
|
|
@@ -37,6 +37,12 @@ from okstra_ctl.models import (
|
|
|
37
37
|
resolve_model_metadata,
|
|
38
38
|
)
|
|
39
39
|
from okstra_ctl.clarification_items import scan_approval_gate, user_response_sidecars
|
|
40
|
+
from okstra_ctl.design_prep import (
|
|
41
|
+
DesignPrepError,
|
|
42
|
+
load_design_prep_items,
|
|
43
|
+
resolve_design_prep,
|
|
44
|
+
write_design_prep_input,
|
|
45
|
+
)
|
|
40
46
|
from okstra_ctl.final_report_paths import final_report_data_path
|
|
41
47
|
from okstra_ctl.pr_template import PrTemplateError, resolve_pr_template_path
|
|
42
48
|
from okstra_ctl.run import (
|
|
@@ -303,6 +309,9 @@ S_BASE_REF_TEXT = "base_ref_text"
|
|
|
303
309
|
S_APPROVED_PLAN_PICK = "approved_plan_pick"
|
|
304
310
|
S_APPROVED_PLAN = "approved_plan"
|
|
305
311
|
S_APPROVE_PLAN_CONFIRM = "approve_plan_confirm"
|
|
312
|
+
S_DESIGN_PREP_DECISION = "design_prep_decision"
|
|
313
|
+
S_DESIGN_PREP_OVERRIDES = "design_prep_overrides"
|
|
314
|
+
S_DESIGN_PREP_CONFIRM = "design_prep_confirm"
|
|
306
315
|
S_STAGE_PICK = "stage_pick"
|
|
307
316
|
S_HANDOFF_STAGE_PICK = "handoff_stage_pick"
|
|
308
317
|
S_EXECUTOR = "executor"
|
|
@@ -403,6 +412,11 @@ class WizardState:
|
|
|
403
412
|
# approve-confirm 단계가 3-옵션(yes_apply/yes/no)으로 확장된다.
|
|
404
413
|
html_approval_sidecar: str = ""
|
|
405
414
|
html_approval_option: str = ""
|
|
415
|
+
design_prep_queue: list[str] = field(default_factory=list)
|
|
416
|
+
design_prep_current: str = ""
|
|
417
|
+
design_prep_decision: str = ""
|
|
418
|
+
design_prep_overrides_json: str = ""
|
|
419
|
+
design_prep_notes: str = ""
|
|
406
420
|
selected_stage: str = "auto"
|
|
407
421
|
selected_stages: str = "" # implementation 다중선택 위상정렬 CSV
|
|
408
422
|
executor: str = ""
|
|
@@ -1077,6 +1091,7 @@ class Step:
|
|
|
1077
1091
|
submit: Callable[[WizardState, str], Optional[str]]
|
|
1078
1092
|
# Field names this step owns; resetting the step clears them.
|
|
1079
1093
|
owns: tuple[str, ...] = ()
|
|
1094
|
+
repeatable: bool = False
|
|
1080
1095
|
|
|
1081
1096
|
|
|
1082
1097
|
def _opt(value: str, label: str = "", description: str = "") -> Option:
|
|
@@ -1939,6 +1954,234 @@ def _submit_approve_plan_confirm(state: WizardState, value: str) -> Optional[str
|
|
|
1939
1954
|
return echo
|
|
1940
1955
|
|
|
1941
1956
|
|
|
1957
|
+
def _design_prep_items_by_id(state: WizardState) -> dict[str, dict[str, Any]]:
|
|
1958
|
+
try:
|
|
1959
|
+
items = load_design_prep_items(Path(state.approved_plan_path))
|
|
1960
|
+
except DesignPrepError as exc:
|
|
1961
|
+
raise WizardError(str(exc)) from exc
|
|
1962
|
+
return {str(item["id"]): item for item in items}
|
|
1963
|
+
|
|
1964
|
+
|
|
1965
|
+
def _ensure_design_prep_queue(state: WizardState) -> None:
|
|
1966
|
+
if state.design_prep_current or state.design_prep_queue:
|
|
1967
|
+
return
|
|
1968
|
+
if S_DESIGN_PREP_DECISION in state.answered:
|
|
1969
|
+
return
|
|
1970
|
+
report_path = Path(state.approved_plan_path)
|
|
1971
|
+
if not re.fullmatch(
|
|
1972
|
+
r"final-report-implementation-planning-\d+\.md",
|
|
1973
|
+
report_path.name,
|
|
1974
|
+
):
|
|
1975
|
+
return
|
|
1976
|
+
data_path = report_path.with_name(
|
|
1977
|
+
report_path.name.removesuffix(".md") + ".data.json"
|
|
1978
|
+
)
|
|
1979
|
+
if not data_path.is_file():
|
|
1980
|
+
return
|
|
1981
|
+
try:
|
|
1982
|
+
items = load_design_prep_items(report_path)
|
|
1983
|
+
effective = resolve_design_prep(report_path)
|
|
1984
|
+
except DesignPrepError as exc:
|
|
1985
|
+
raise WizardError(str(exc)) from exc
|
|
1986
|
+
decisions = {
|
|
1987
|
+
str(item["id"]): item.get("decision")
|
|
1988
|
+
for item in effective.effective_items
|
|
1989
|
+
}
|
|
1990
|
+
unresolved = [
|
|
1991
|
+
item for item in items
|
|
1992
|
+
if item.get("status") in ("blocked", "provisional")
|
|
1993
|
+
and decisions.get(str(item["id"])) == "unanswered"
|
|
1994
|
+
]
|
|
1995
|
+
unresolved.sort(key=lambda item: item.get("status") != "blocked")
|
|
1996
|
+
state.design_prep_queue = [str(item["id"]) for item in unresolved]
|
|
1997
|
+
_advance_design_prep_item(state)
|
|
1998
|
+
|
|
1999
|
+
|
|
2000
|
+
def _advance_design_prep_item(state: WizardState) -> None:
|
|
2001
|
+
state.design_prep_current = (
|
|
2002
|
+
state.design_prep_queue.pop(0) if state.design_prep_queue else ""
|
|
2003
|
+
)
|
|
2004
|
+
state.design_prep_decision = ""
|
|
2005
|
+
state.design_prep_overrides_json = ""
|
|
2006
|
+
state.design_prep_notes = ""
|
|
2007
|
+
|
|
2008
|
+
|
|
2009
|
+
def _current_design_prep_item(state: WizardState) -> dict[str, Any]:
|
|
2010
|
+
item = _design_prep_items_by_id(state).get(state.design_prep_current)
|
|
2011
|
+
if item is None:
|
|
2012
|
+
raise WizardError(
|
|
2013
|
+
f"design preparation item is missing: {state.design_prep_current}"
|
|
2014
|
+
)
|
|
2015
|
+
return item
|
|
2016
|
+
|
|
2017
|
+
|
|
2018
|
+
def _design_prep_decision_applies(state: WizardState) -> bool:
|
|
2019
|
+
if state.task_type != "implementation" or not state.approved_plan_path:
|
|
2020
|
+
return False
|
|
2021
|
+
_ensure_design_prep_queue(state)
|
|
2022
|
+
return bool(state.design_prep_current and not state.design_prep_decision)
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
def _build_design_prep_decision(state: WizardState) -> Prompt:
|
|
2026
|
+
item = _current_design_prep_item(state)
|
|
2027
|
+
proposal = item.get("aiProposal") or {}
|
|
2028
|
+
t = _p(
|
|
2029
|
+
state.workspace_root,
|
|
2030
|
+
S_DESIGN_PREP_DECISION,
|
|
2031
|
+
item_id=str(item["id"]),
|
|
2032
|
+
title=str(item["title"]),
|
|
2033
|
+
stage_refs=json.dumps(item.get("stageRefs") or [], ensure_ascii=False),
|
|
2034
|
+
proposal_summary=str(proposal.get("summary") or ""),
|
|
2035
|
+
details=json.dumps(proposal.get("details") or [], ensure_ascii=False),
|
|
2036
|
+
assumptions=json.dumps(proposal.get("assumptions") or [], ensure_ascii=False),
|
|
2037
|
+
guardrails=json.dumps(item.get("guardrails") or [], ensure_ascii=False),
|
|
2038
|
+
request_path=str(item.get("requestPath") or ""),
|
|
2039
|
+
)
|
|
2040
|
+
return Prompt(
|
|
2041
|
+
step=S_DESIGN_PREP_DECISION,
|
|
2042
|
+
kind="pick",
|
|
2043
|
+
label=t["label"],
|
|
2044
|
+
options=[_opt(value, label) for value, label in _static_options(t)],
|
|
2045
|
+
echo_template=t["echo_template"],
|
|
2046
|
+
)
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
def _submit_design_prep_decision(
|
|
2050
|
+
state: WizardState,
|
|
2051
|
+
value: str,
|
|
2052
|
+
) -> Optional[str]:
|
|
2053
|
+
t = _p(state.workspace_root, S_DESIGN_PREP_DECISION,
|
|
2054
|
+
item_id="", title="", stage_refs="", proposal_summary="",
|
|
2055
|
+
details="", assumptions="", guardrails="", request_path="")
|
|
2056
|
+
if value not in ("accept-draft", "modify-draft", "reject-draft", "later"):
|
|
2057
|
+
raise WizardError(t["errors"]["invalid_decision"])
|
|
2058
|
+
if value == "later":
|
|
2059
|
+
item_id = state.design_prep_current
|
|
2060
|
+
_advance_design_prep_item(state)
|
|
2061
|
+
return t["echo_variants"]["later"].format(item_id=item_id)
|
|
2062
|
+
state.design_prep_decision = value
|
|
2063
|
+
state.design_prep_overrides_json = ""
|
|
2064
|
+
state.design_prep_notes = ""
|
|
2065
|
+
return t["echo_template"].format(value=value)
|
|
2066
|
+
|
|
2067
|
+
|
|
2068
|
+
def _design_prep_overrides_applies(state: WizardState) -> bool:
|
|
2069
|
+
if state.design_prep_decision == "modify-draft":
|
|
2070
|
+
return not state.design_prep_overrides_json
|
|
2071
|
+
if state.design_prep_decision == "reject-draft":
|
|
2072
|
+
return not state.design_prep_notes
|
|
2073
|
+
return False
|
|
2074
|
+
|
|
2075
|
+
|
|
2076
|
+
def _build_design_prep_overrides(state: WizardState) -> Prompt:
|
|
2077
|
+
t = _p(
|
|
2078
|
+
state.workspace_root,
|
|
2079
|
+
S_DESIGN_PREP_OVERRIDES,
|
|
2080
|
+
item_id=state.design_prep_current,
|
|
2081
|
+
decision=state.design_prep_decision,
|
|
2082
|
+
)
|
|
2083
|
+
return Prompt(
|
|
2084
|
+
step=S_DESIGN_PREP_OVERRIDES,
|
|
2085
|
+
kind="text",
|
|
2086
|
+
label=t["label"],
|
|
2087
|
+
echo_template=t["echo_template"],
|
|
2088
|
+
)
|
|
2089
|
+
|
|
2090
|
+
|
|
2091
|
+
def _submit_design_prep_overrides(
|
|
2092
|
+
state: WizardState,
|
|
2093
|
+
value: str,
|
|
2094
|
+
) -> Optional[str]:
|
|
2095
|
+
t = _p(state.workspace_root, S_DESIGN_PREP_OVERRIDES,
|
|
2096
|
+
item_id=state.design_prep_current,
|
|
2097
|
+
decision=state.design_prep_decision)
|
|
2098
|
+
if state.design_prep_decision == "reject-draft":
|
|
2099
|
+
if not value.strip():
|
|
2100
|
+
raise WizardError(t["errors"]["note_required"])
|
|
2101
|
+
state.design_prep_notes = value.strip()
|
|
2102
|
+
return t["echo_variants"]["note"]
|
|
2103
|
+
try:
|
|
2104
|
+
overrides = json.loads(value)
|
|
2105
|
+
except json.JSONDecodeError as exc:
|
|
2106
|
+
raise WizardError(t["errors"]["invalid_json"].format(error=exc)) from exc
|
|
2107
|
+
if not isinstance(overrides, dict):
|
|
2108
|
+
raise WizardError(t["errors"]["object_required"])
|
|
2109
|
+
state.design_prep_overrides_json = json.dumps(
|
|
2110
|
+
overrides, ensure_ascii=False, sort_keys=True
|
|
2111
|
+
)
|
|
2112
|
+
return t["echo_variants"]["overrides"]
|
|
2113
|
+
|
|
2114
|
+
|
|
2115
|
+
def _design_prep_confirm_applies(state: WizardState) -> bool:
|
|
2116
|
+
if state.design_prep_decision == "accept-draft":
|
|
2117
|
+
return True
|
|
2118
|
+
if state.design_prep_decision == "modify-draft":
|
|
2119
|
+
return bool(state.design_prep_overrides_json)
|
|
2120
|
+
if state.design_prep_decision == "reject-draft":
|
|
2121
|
+
return bool(state.design_prep_notes)
|
|
2122
|
+
return False
|
|
2123
|
+
|
|
2124
|
+
|
|
2125
|
+
def _build_design_prep_confirm(state: WizardState) -> Prompt:
|
|
2126
|
+
item = _current_design_prep_item(state)
|
|
2127
|
+
t = _p(
|
|
2128
|
+
state.workspace_root,
|
|
2129
|
+
S_DESIGN_PREP_CONFIRM,
|
|
2130
|
+
item_id=state.design_prep_current,
|
|
2131
|
+
decision=state.design_prep_decision,
|
|
2132
|
+
item_snapshot=json.dumps(item, ensure_ascii=False, indent=2, sort_keys=True),
|
|
2133
|
+
overrides=state.design_prep_overrides_json or "{}",
|
|
2134
|
+
notes=state.design_prep_notes or "(none)",
|
|
2135
|
+
)
|
|
2136
|
+
return Prompt(
|
|
2137
|
+
step=S_DESIGN_PREP_CONFIRM,
|
|
2138
|
+
kind="pick",
|
|
2139
|
+
label=t["label"],
|
|
2140
|
+
options=[_opt(value, label) for value, label in _static_options(t)],
|
|
2141
|
+
echo_template=t["echo_template"],
|
|
2142
|
+
)
|
|
2143
|
+
|
|
2144
|
+
|
|
2145
|
+
def _submit_design_prep_confirm(
|
|
2146
|
+
state: WizardState,
|
|
2147
|
+
value: str,
|
|
2148
|
+
) -> Optional[str]:
|
|
2149
|
+
item_id = state.design_prep_current
|
|
2150
|
+
t = _p(state.workspace_root, S_DESIGN_PREP_CONFIRM,
|
|
2151
|
+
item_id=item_id, decision=state.design_prep_decision,
|
|
2152
|
+
item_snapshot="", overrides="", notes="")
|
|
2153
|
+
if value == "no":
|
|
2154
|
+
state.design_prep_decision = ""
|
|
2155
|
+
state.design_prep_overrides_json = ""
|
|
2156
|
+
state.design_prep_notes = ""
|
|
2157
|
+
return t["echo_variants"]["revise"].format(item_id=item_id)
|
|
2158
|
+
if value != "yes":
|
|
2159
|
+
raise WizardError(t["errors"]["confirmation_required"])
|
|
2160
|
+
overrides = json.loads(state.design_prep_overrides_json or "{}")
|
|
2161
|
+
try:
|
|
2162
|
+
path = write_design_prep_input(
|
|
2163
|
+
Path(state.approved_plan_path),
|
|
2164
|
+
item_id,
|
|
2165
|
+
state.design_prep_decision,
|
|
2166
|
+
overrides,
|
|
2167
|
+
state.design_prep_notes,
|
|
2168
|
+
captured_by="okstra-wizard",
|
|
2169
|
+
)
|
|
2170
|
+
except DesignPrepError as exc:
|
|
2171
|
+
raise WizardError(str(exc)) from exc
|
|
2172
|
+
match = re.search(r"-r(\d+)-([0-9a-f-]{36})\.md$", path.name)
|
|
2173
|
+
if match is None:
|
|
2174
|
+
raise WizardError(f"design preparation writer returned invalid path: {path}")
|
|
2175
|
+
echo = t["echo_variants"]["written"].format(
|
|
2176
|
+
item_id=item_id,
|
|
2177
|
+
revision=int(match.group(1)),
|
|
2178
|
+
input_id=match.group(2),
|
|
2179
|
+
path=path,
|
|
2180
|
+
)
|
|
2181
|
+
_advance_design_prep_item(state)
|
|
2182
|
+
return echo
|
|
2183
|
+
|
|
2184
|
+
|
|
1942
2185
|
def _build_stage_pick(state: WizardState) -> Prompt:
|
|
1943
2186
|
"""Parse the Stage Map from the approved plan and build the stage picker."""
|
|
1944
2187
|
t = _p(state.workspace_root, "stage_pick")
|
|
@@ -3116,6 +3359,27 @@ STEPS: list[Step] = [
|
|
|
3116
3359
|
and S_APPROVE_PLAN_CONFIRM not in s.answered),
|
|
3117
3360
|
build=_build_approve_plan_confirm, submit=_submit_approve_plan_confirm,
|
|
3118
3361
|
owns=("approve_plan_candidate",)),
|
|
3362
|
+
Step(S_DESIGN_PREP_DECISION,
|
|
3363
|
+
applies=_design_prep_decision_applies,
|
|
3364
|
+
build=_build_design_prep_decision,
|
|
3365
|
+
submit=_submit_design_prep_decision,
|
|
3366
|
+
owns=("design_prep_queue", "design_prep_current",
|
|
3367
|
+
"design_prep_decision", "design_prep_overrides_json",
|
|
3368
|
+
"design_prep_notes"),
|
|
3369
|
+
repeatable=True),
|
|
3370
|
+
Step(S_DESIGN_PREP_OVERRIDES,
|
|
3371
|
+
applies=_design_prep_overrides_applies,
|
|
3372
|
+
build=_build_design_prep_overrides,
|
|
3373
|
+
submit=_submit_design_prep_overrides,
|
|
3374
|
+
owns=("design_prep_overrides_json", "design_prep_notes"),
|
|
3375
|
+
repeatable=True),
|
|
3376
|
+
Step(S_DESIGN_PREP_CONFIRM,
|
|
3377
|
+
applies=_design_prep_confirm_applies,
|
|
3378
|
+
build=_build_design_prep_confirm,
|
|
3379
|
+
submit=_submit_design_prep_confirm,
|
|
3380
|
+
owns=("design_prep_decision", "design_prep_overrides_json",
|
|
3381
|
+
"design_prep_notes"),
|
|
3382
|
+
repeatable=True),
|
|
3119
3383
|
Step(S_STAGE_PICK,
|
|
3120
3384
|
applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
|
|
3121
3385
|
and bool(s.approved_plan_path)
|
|
@@ -3374,6 +3638,9 @@ _FIELD_DEFAULTS: dict[str, Any] = {
|
|
|
3374
3638
|
"base_ref_pending_text": False, "approved_plan_path": "",
|
|
3375
3639
|
"approved_plan_pending_text": False, "approve_plan_candidate": "",
|
|
3376
3640
|
"html_approval_sidecar": "", "html_approval_option": "",
|
|
3641
|
+
"design_prep_queue": [], "design_prep_current": "",
|
|
3642
|
+
"design_prep_decision": "", "design_prep_overrides_json": "",
|
|
3643
|
+
"design_prep_notes": "",
|
|
3377
3644
|
"selected_stage": "auto",
|
|
3378
3645
|
"selected_stages": "",
|
|
3379
3646
|
"handoff_mode": "", "handoff_stages": "",
|
|
@@ -3424,9 +3691,9 @@ def _build_group_prompt(state: WizardState, group_id: str) -> Prompt:
|
|
|
3424
3691
|
"""
|
|
3425
3692
|
members: list[Prompt] = []
|
|
3426
3693
|
for sid in PROMPT_GROUPS[group_id]:
|
|
3427
|
-
if sid in state.answered:
|
|
3428
|
-
continue
|
|
3429
3694
|
step = STEP_BY_ID[sid]
|
|
3695
|
+
if sid in state.answered and not step.repeatable:
|
|
3696
|
+
continue
|
|
3430
3697
|
if not step.applies(state):
|
|
3431
3698
|
continue
|
|
3432
3699
|
members.append(step.build(state))
|
|
@@ -3445,7 +3712,7 @@ def next_prompt(state: WizardState) -> Prompt:
|
|
|
3445
3712
|
if state.confirmed:
|
|
3446
3713
|
return Prompt(step=S_DONE, kind="done")
|
|
3447
3714
|
for step in STEPS:
|
|
3448
|
-
if step.id in state.answered:
|
|
3715
|
+
if step.id in state.answered and not step.repeatable:
|
|
3449
3716
|
continue
|
|
3450
3717
|
if step.applies(state):
|
|
3451
3718
|
group_id = _STEP_TO_GROUP.get(step.id)
|
|
@@ -3488,6 +3755,19 @@ def _sim_advance(state: WizardState, prompt: Prompt) -> None:
|
|
|
3488
3755
|
if q.step not in state.answered:
|
|
3489
3756
|
state.answered.append(q.step)
|
|
3490
3757
|
return
|
|
3758
|
+
if prompt.step == S_DESIGN_PREP_CONFIRM:
|
|
3759
|
+
_advance_design_prep_item(state)
|
|
3760
|
+
if prompt.step not in state.answered:
|
|
3761
|
+
state.answered.append(prompt.step)
|
|
3762
|
+
return
|
|
3763
|
+
if prompt.step == S_DESIGN_PREP_OVERRIDES:
|
|
3764
|
+
if state.design_prep_decision == "modify-draft":
|
|
3765
|
+
state.design_prep_overrides_json = "{}"
|
|
3766
|
+
else:
|
|
3767
|
+
state.design_prep_notes = "simulation"
|
|
3768
|
+
if prompt.step not in state.answered:
|
|
3769
|
+
state.answered.append(prompt.step)
|
|
3770
|
+
return
|
|
3491
3771
|
STEP_BY_ID[prompt.step].submit(state, _sim_answer(prompt))
|
|
3492
3772
|
if prompt.step not in state.answered:
|
|
3493
3773
|
state.answered.append(prompt.step)
|
|
@@ -14,6 +14,10 @@ _HERE = Path(__file__).resolve().parent.parent
|
|
|
14
14
|
if str(_HERE) not in sys.path:
|
|
15
15
|
sys.path.insert(0, str(_HERE))
|
|
16
16
|
|
|
17
|
+
from okstra_ctl.design_prep import ( # noqa: E402
|
|
18
|
+
DesignPrepError,
|
|
19
|
+
materialize_design_prep_requests,
|
|
20
|
+
)
|
|
17
21
|
from okstra_ctl.final_report_paths import final_report_markdown_path # noqa: E402
|
|
18
22
|
from okstra_ctl.render_final_report import ( # noqa: E402
|
|
19
23
|
FinalReportRenderError,
|
|
@@ -216,4 +220,11 @@ def populate_and_render(
|
|
|
216
220
|
bytes_written = render_to_file(data_path, output_path)
|
|
217
221
|
except FinalReportRenderError as exc:
|
|
218
222
|
raise SubstituteRefusedError(f"render after substitute failed: {exc}") from exc
|
|
223
|
+
if data_path.name.startswith("final-report-implementation-planning-"):
|
|
224
|
+
try:
|
|
225
|
+
materialize_design_prep_requests(data_path)
|
|
226
|
+
except DesignPrepError as exc:
|
|
227
|
+
raise SubstituteRefusedError(
|
|
228
|
+
f"design-prep request materialization failed: {exc}"
|
|
229
|
+
) from exc
|
|
219
230
|
return changes, bytes_written
|