okstra 0.121.1 → 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.
Files changed (67) hide show
  1. package/README.md +4 -2
  2. package/docs/architecture/storage-model.md +14 -0
  3. package/docs/architecture.md +38 -8
  4. package/docs/cli.md +45 -5
  5. package/docs/for-ai/README.md +2 -2
  6. package/docs/for-ai/skills/okstra-rollup.md +1 -1
  7. package/docs/for-ai/skills/{okstra-schedule.md → okstra-schedule-gen.md} +3 -3
  8. package/docs/project-structure-overview.md +4 -3
  9. package/docs/task-process/implementation-planning.md +33 -9
  10. package/docs/task-process/implementation.md +21 -2
  11. package/package.json +1 -1
  12. package/runtime/BUILD.json +2 -2
  13. package/runtime/agents/workers/claude-worker.md +1 -1
  14. package/runtime/bin/lib/okstra/usage.sh +3 -3
  15. package/runtime/prompts/launch.template.md +9 -6
  16. package/runtime/prompts/lead/adapters/claude-code.md +104 -0
  17. package/runtime/prompts/lead/adapters/codex.md +45 -0
  18. package/runtime/prompts/lead/adapters/external.md +46 -0
  19. package/runtime/prompts/lead/context-loader.md +5 -5
  20. package/runtime/prompts/lead/convergence.md +11 -28
  21. package/runtime/prompts/lead/okstra-lead-contract.md +44 -94
  22. package/runtime/prompts/lead/plan-body-verification.md +50 -5
  23. package/runtime/prompts/lead/report-writer.md +59 -54
  24. package/runtime/prompts/lead/team-contract.md +33 -106
  25. package/runtime/prompts/profiles/_common-contract.md +3 -3
  26. package/runtime/prompts/profiles/_implementation-deliverable.md +3 -1
  27. package/runtime/prompts/profiles/_implementation-executor.md +6 -1
  28. package/runtime/prompts/profiles/_implementation-verifier.md +2 -2
  29. package/runtime/prompts/profiles/final-verification.md +2 -0
  30. package/runtime/prompts/profiles/implementation-planning.md +14 -4
  31. package/runtime/prompts/profiles/requirements-discovery.md +1 -1
  32. package/runtime/prompts/wizard/prompts.ko.json +44 -0
  33. package/runtime/python/okstra_ctl/codex_dispatch.py +23 -1
  34. package/runtime/python/okstra_ctl/design_prep.py +1462 -0
  35. package/runtime/python/okstra_ctl/design_surfaces.py +243 -0
  36. package/runtime/python/okstra_ctl/final_report_schema.py +33 -1
  37. package/runtime/python/okstra_ctl/implementation_stage.py +35 -0
  38. package/runtime/python/okstra_ctl/incremental_carry.py +294 -21
  39. package/runtime/python/okstra_ctl/incremental_scope.py +51 -5
  40. package/runtime/python/okstra_ctl/lead_runtime.py +4 -0
  41. package/runtime/python/okstra_ctl/material.py +1 -1
  42. package/runtime/python/okstra_ctl/model_discovery.py +98 -0
  43. package/runtime/python/okstra_ctl/models.py +8 -3
  44. package/runtime/python/okstra_ctl/render.py +81 -194
  45. package/runtime/python/okstra_ctl/report_views.py +22 -7
  46. package/runtime/python/okstra_ctl/run.py +53 -5
  47. package/runtime/python/okstra_ctl/team_reconcile.py +3 -1
  48. package/runtime/python/okstra_ctl/user_response.py +67 -2
  49. package/runtime/python/okstra_ctl/wizard.py +283 -3
  50. package/runtime/python/okstra_token_usage/report.py +11 -0
  51. package/runtime/schemas/final-report-v1.0.schema.json +339 -0
  52. package/runtime/skills/okstra-inspect/SKILL.md +2 -2
  53. package/runtime/skills/okstra-rollup/SKILL.md +1 -1
  54. package/runtime/skills/{okstra-schedule → okstra-schedule-gen}/SKILL.md +62 -20
  55. package/runtime/skills/okstra-user-response/SKILL.md +8 -6
  56. package/runtime/templates/reports/final-report.template.md +71 -4
  57. package/runtime/templates/reports/i18n/en.json +32 -1
  58. package/runtime/templates/reports/i18n/ko.json +32 -1
  59. package/runtime/validators/validate-run.py +515 -5
  60. package/runtime/validators/validate-schedule.py +11 -7
  61. package/runtime/validators/validate_session_conformance.py +58 -33
  62. package/src/cli-registry.mjs +14 -0
  63. package/src/commands/inspect/design-prep.mjs +23 -0
  64. package/src/commands/inspect/stage-map.mjs +100 -0
  65. package/src/commands/lifecycle/install.mjs +3 -0
  66. package/src/commands/lifecycle/uninstall.mjs +8 -23
  67. package/src/lib/skill-catalog.mjs +10 -1
@@ -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