okstra 0.172.0 → 0.173.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 (83) hide show
  1. package/README.md +8 -6
  2. package/docs/architecture/storage-model.md +11 -0
  3. package/docs/architecture.md +16 -14
  4. package/docs/cli.md +36 -5
  5. package/docs/performance-improvement-plan-v2.md +6 -5
  6. package/docs/project-structure-overview.md +21 -13
  7. package/docs/task-process/README.md +5 -3
  8. package/docs/task-process/error-analysis.md +2 -2
  9. package/docs/task-process/final-verification.md +2 -2
  10. package/docs/task-process/implementation-option-selection.md +70 -0
  11. package/docs/task-process/implementation-planning.md +23 -15
  12. package/docs/task-process/requirements-discovery.md +2 -2
  13. package/package.json +1 -1
  14. package/runtime/BUILD.json +2 -2
  15. package/runtime/agents/workers/report-writer-worker.md +30 -6
  16. package/runtime/bin/lib/okstra/cli.sh +5 -1
  17. package/runtime/bin/lib/okstra/globals.sh +1 -0
  18. package/runtime/bin/lib/okstra/usage.sh +3 -0
  19. package/runtime/bin/okstra.sh +2 -0
  20. package/runtime/prompts/duties/direction-selection-worker.md +44 -0
  21. package/runtime/prompts/duties/planning-worker.md +12 -4
  22. package/runtime/prompts/lead/context-loader.md +1 -1
  23. package/runtime/prompts/lead/convergence.md +5 -5
  24. package/runtime/prompts/lead/okstra-lead-contract.md +6 -5
  25. package/runtime/prompts/lead/plan-body-verification.md +20 -3
  26. package/runtime/prompts/lead/report-writer.md +27 -5
  27. package/runtime/prompts/profiles/_common-contract.md +1 -1
  28. package/runtime/prompts/profiles/_implementation-deliverable.md +2 -2
  29. package/runtime/prompts/profiles/error-analysis.md +3 -3
  30. package/runtime/prompts/profiles/final-verification.md +3 -3
  31. package/runtime/prompts/profiles/forbidden-actions.json +7 -0
  32. package/runtime/prompts/profiles/implementation-option-selection.md +35 -0
  33. package/runtime/prompts/profiles/implementation-planning.md +50 -38
  34. package/runtime/prompts/profiles/implementation.md +2 -1
  35. package/runtime/prompts/profiles/improvement-discovery.md +1 -1
  36. package/runtime/prompts/profiles/requirements-discovery.md +3 -3
  37. package/runtime/prompts/wizard/prompts.ko.json +9 -1
  38. package/runtime/python/okstra_ctl/agent_invocation.py +1 -0
  39. package/runtime/python/okstra_ctl/analysis_packet.py +6 -0
  40. package/runtime/python/okstra_ctl/exact_coverage.py +128 -0
  41. package/runtime/python/okstra_ctl/fix_cycles.py +3 -1
  42. package/runtime/python/okstra_ctl/implementation_direction.py +836 -0
  43. package/runtime/python/okstra_ctl/implementation_options.py +479 -0
  44. package/runtime/python/okstra_ctl/plan_items.py +51 -3
  45. package/runtime/python/okstra_ctl/render.py +1 -0
  46. package/runtime/python/okstra_ctl/render_final_report.py +1 -0
  47. package/runtime/python/okstra_ctl/report_contract.py +45 -13
  48. package/runtime/python/okstra_ctl/report_html/render.py +4 -2
  49. package/runtime/python/okstra_ctl/report_html/router.py +4 -0
  50. package/runtime/python/okstra_ctl/report_html/view_models/implementation_option_selection.py +32 -0
  51. package/runtime/python/okstra_ctl/report_html/view_models/implementation_planning.py +25 -10
  52. package/runtime/python/okstra_ctl/report_views.py +148 -12
  53. package/runtime/python/okstra_ctl/run.py +350 -2
  54. package/runtime/python/okstra_ctl/scope_provenance.py +15 -9
  55. package/runtime/python/okstra_ctl/user_response.py +75 -0
  56. package/runtime/python/okstra_ctl/wizard.py +144 -0
  57. package/runtime/python/okstra_ctl/worker_prompt_policy.py +2 -0
  58. package/runtime/python/okstra_ctl/workflow.py +29 -7
  59. package/runtime/schemas/final-report-v2.0.schema.json +1428 -137
  60. package/runtime/templates/reports/final-report-v2.template.md +4 -0
  61. package/runtime/templates/reports/final-verification-input.template.md +1 -1
  62. package/runtime/templates/reports/html/base.template.html +3 -2
  63. package/runtime/templates/reports/html/i18n/en.json +21 -1
  64. package/runtime/templates/reports/html/i18n/ko.json +21 -1
  65. package/runtime/templates/reports/html/macros/forms.html +21 -2
  66. package/runtime/templates/reports/html/tasks/implementation-option-selection.template.html +49 -0
  67. package/runtime/templates/reports/html/tasks/implementation-planning.template.html +36 -2
  68. package/runtime/templates/reports/i18n/en.json +13 -0
  69. package/runtime/templates/reports/implementation-input.template.md +4 -2
  70. package/runtime/templates/reports/implementation-planning-input.template.md +18 -4
  71. package/runtime/templates/reports/improvement-discovery-input.template.md +1 -1
  72. package/runtime/templates/reports/md/tasks/implementation-option-selection.template.md +13 -0
  73. package/runtime/templates/reports/md/tasks/implementation-planning.template.md +17 -0
  74. package/runtime/templates/reports/report.js +111 -4
  75. package/runtime/templates/reports/task-brief.template.md +9 -3
  76. package/runtime/templates/reports/user-response.template.md +25 -4
  77. package/runtime/templates/worker-prompt-preamble.md +8 -0
  78. package/runtime/validators/validate-implementation-plan-stages.py +106 -1
  79. package/runtime/validators/validate-report-views.py +2 -2
  80. package/runtime/validators/validate-run.py +135 -25
  81. package/runtime/validators/validate_improvement_report.py +5 -1
  82. package/src/commands/execute/codex-run.mjs +1 -0
  83. package/src/commands/execute/render-bundle.mjs +1 -0
@@ -82,6 +82,11 @@ from okstra_ctl.design_prep import (
82
82
  resolve_design_prep,
83
83
  write_design_prep_input,
84
84
  )
85
+ from okstra_ctl.implementation_direction import (
86
+ DirectionSelectionError,
87
+ lexical_absolute_path,
88
+ validate_task_artifact_path,
89
+ )
85
90
  from okstra_ctl.final_report_paths import final_report_data_path
86
91
  from okstra_ctl.plan_run_root import list_implementation_planning_reports
87
92
  from okstra_ctl.pr_template import PrTemplateError, resolve_pr_template_path
@@ -154,6 +159,7 @@ TASK_TYPES: list[tuple[str, str]] = [
154
159
  ("improvement-discovery", "Find improvement candidates within a codebase scope and lens whitelist"),
155
160
  *zip(ANALYSIS_TASK_TYPES, _ANALYSIS_TASK_TYPE_DESCRIPTIONS),
156
161
  ("error-analysis", "Evidence-based root-cause analysis (no code changes)"),
162
+ ("implementation-option-selection", "Compare implementation options (read-only)"),
157
163
  ("implementation-planning", "Plan options + request user approval"),
158
164
  ("implementation", "Execute approved plan (requires approved final-report)"),
159
165
  ("final-verification", "Acceptance + residual-risk review"),
@@ -331,6 +337,7 @@ S_ANALYSIS_TARGET_PICK = "analysis_target_pick"
331
337
  S_ANALYSIS_TARGET = "analysis_target"
332
338
  S_BASE_REF_PICK = "base_ref_pick"
333
339
  S_BASE_REF_TEXT = "base_ref_text"
340
+ S_SELECTED_DIRECTION_PICK = "selected_direction_pick"
334
341
  S_APPROVED_PLAN_PICK = "approved_plan_pick"
335
342
  S_APPROVED_PLAN = "approved_plan"
336
343
  S_APPROVE_PLAN_CONFIRM = "approve_plan_confirm"
@@ -495,6 +502,7 @@ class WizardState:
495
502
  clarification_response_path: str = ""
496
503
  clarification_pending_text: bool = False
497
504
  last_final_report_cached: str = ""
505
+ selected_direction_path: str = ""
498
506
  # "" | "auto" | "full" | "<stage csv>" — 사용자가 고른 이번 재실행의 재검증
499
507
  # 범위. implementation-planning 재실행에서 좁힐 여지가 있을 때만 채워진다.
500
508
  reverify_scope: str = ""
@@ -595,6 +603,105 @@ def _require_file(path_str: str, project_root: Path, label: str) -> Path:
595
603
  return p
596
604
 
597
605
 
606
+ _SELECTION_REPORT_RE = re.compile(
607
+ r"^final-report-implementation-option-selection-(?P<seq>\d{3,})\.md$"
608
+ )
609
+
610
+
611
+ def _selected_direction_candidates(state: WizardState) -> list[str]:
612
+ if not state.project_root or not state.task_group or not state.task_id:
613
+ return []
614
+ project_root = Path(state.project_root).resolve()
615
+ task_root = lexical_absolute_path(
616
+ task_dir(project_root, state.task_group, state.task_id)
617
+ )
618
+ reports = (
619
+ task_runs_dir(project_root, state.task_group, state.task_id)
620
+ / "implementation-option-selection"
621
+ / "reports"
622
+ )
623
+ candidates: list[tuple[int, Path]] = []
624
+ for report in reports.glob("final-report-implementation-option-selection-*.md"):
625
+ match = _SELECTION_REPORT_RE.fullmatch(report.name)
626
+ if match is None:
627
+ continue
628
+ try:
629
+ validated = validate_task_artifact_path(
630
+ report, task_root, "selection report"
631
+ )
632
+ except DirectionSelectionError:
633
+ continue
634
+ candidates.append((int(match.group("seq")), validated))
635
+ return [
636
+ _project_relative_path(path, project_root)
637
+ for _, path in sorted(candidates, reverse=True)[:3]
638
+ ]
639
+
640
+
641
+ def _planning_rerun_selected(state: WizardState) -> bool:
642
+ if (
643
+ not state.clarification_response_path
644
+ or not state.project_root
645
+ or not state.task_group
646
+ or not state.task_id
647
+ ):
648
+ return False
649
+ project_root = Path(state.project_root).resolve()
650
+ raw_path = Path(state.clarification_response_path).expanduser()
651
+ path = lexical_absolute_path(
652
+ raw_path if raw_path.is_absolute() else project_root / raw_path
653
+ )
654
+ task_root = lexical_absolute_path(
655
+ task_dir(project_root, state.task_group, state.task_id)
656
+ )
657
+ reports = lexical_absolute_path(
658
+ task_runs_dir(project_root, state.task_group, state.task_id)
659
+ / "implementation-planning"
660
+ / "reports"
661
+ )
662
+ try:
663
+ validate_task_artifact_path(path, task_root, "planning report")
664
+ except DirectionSelectionError:
665
+ return False
666
+ return (
667
+ path.is_file()
668
+ and not path.is_symlink()
669
+ and re.fullmatch(
670
+ r"final-report-implementation-planning-\d{3,}\.md", path.name
671
+ )
672
+ is not None
673
+ and path.parent == reports
674
+ )
675
+
676
+
677
+ def _build_selected_direction_pick(state: WizardState) -> Prompt:
678
+ candidates = _selected_direction_candidates(state)
679
+ t = _p(state.workspace_root, S_SELECTED_DIRECTION_PICK)
680
+ if not candidates:
681
+ raise WizardError(t["errors"]["none"])
682
+ return Prompt(
683
+ step=S_SELECTED_DIRECTION_PICK,
684
+ kind="pick",
685
+ label=t["label"],
686
+ options=[_opt(path, path) for path in candidates],
687
+ echo_template=t["echo_template"],
688
+ )
689
+
690
+
691
+ def _submit_selected_direction_pick(
692
+ state: WizardState, value: str
693
+ ) -> Optional[str]:
694
+ candidates = _selected_direction_candidates(state)
695
+ if value not in candidates:
696
+ raise WizardError(
697
+ _p(state.workspace_root, S_SELECTED_DIRECTION_PICK)["errors"][
698
+ "unknown"
699
+ ].format(value=value)
700
+ )
701
+ state.selected_direction_path = value
702
+ return f"selected-direction: {value}"
703
+
704
+
598
705
  def _data_json_approved_state(plan_path: Path) -> Optional[bool]:
599
706
  """`approved` flag of the sibling final-report data.json (the SSOT).
600
707
 
@@ -621,6 +728,18 @@ def _classify_approved_plan(path_str: str, project_root: Path) -> tuple[Path, bo
621
728
  ``already_fully_approved=False`` — the approve-confirm step offers to flip it.
622
729
  """
623
730
  p = _require_file(path_str, project_root, "approved plan")
731
+ loaded = _load_final_report_data_if_present(p)
732
+ if loaded is not None:
733
+ planning = loaded[1].get("implementationPlanning")
734
+ if (
735
+ isinstance(planning, dict)
736
+ and planning.get("planningContract") == "selected-direction"
737
+ and planning.get("outcome") == "direction-invalidated"
738
+ ):
739
+ raise WizardError(
740
+ "direction-invalidated planning reports are not approvable; "
741
+ "re-enter implementation-option-selection"
742
+ )
624
743
  body = p.read_text(encoding="utf-8", errors="replace")
625
744
  frontmatter = _extract_frontmatter_block(body)
626
745
  if frontmatter is None:
@@ -690,6 +809,14 @@ def _find_html_approval_sidecar(
690
809
  승인이 아닌 판정(반려·재작업 요청)은 여기서 걸러진다 — 이 단계가 묻는 것은
691
810
  "사용자가 이 plan 을 승인해 두었는가" 뿐이고, 반려 사유는 다음 planning
692
811
  run 이 sidecar 를 통째로 읽어 처리한다."""
812
+ loaded = _load_final_report_data_if_present(plan_path)
813
+ if loaded is not None:
814
+ planning = loaded[1].get("implementationPlanning")
815
+ if (
816
+ isinstance(planning, dict)
817
+ and planning.get("planningContract") == "selected-direction"
818
+ ):
819
+ return None
693
820
  responses_dir = plan_path.parent.parent / "user-responses"
694
821
  if not responses_dir.is_dir():
695
822
  return None
@@ -4175,6 +4302,19 @@ STEPS: list[Step] = [
4175
4302
  applies=lambda s: s.base_ref_pending_text,
4176
4303
  build=_build_base_ref_text, submit=_submit_base_ref_text,
4177
4304
  owns=("base_ref", "base_ref_pending_text")),
4305
+ Step(S_SELECTED_DIRECTION_PICK,
4306
+ applies=lambda s: (
4307
+ s.task_type == "implementation-planning"
4308
+ and not s.selected_direction_path
4309
+ and not _planning_rerun_selected(s)
4310
+ and _brief_resolved(s)
4311
+ and _base_ref_ready(s)
4312
+ and not s.base_ref_pending_text
4313
+ and S_SELECTED_DIRECTION_PICK not in s.answered
4314
+ ),
4315
+ build=_build_selected_direction_pick,
4316
+ submit=_submit_selected_direction_pick,
4317
+ owns=("selected_direction_path",)),
4178
4318
  Step(S_APPROVED_PLAN_PICK,
4179
4319
  applies=lambda s: (s.task_type in _STAGE_SCOPED_TASK_TYPES
4180
4320
  and not s.approved_plan_path
@@ -4556,6 +4696,7 @@ _FIELD_DEFAULTS: dict[str, Any] = {
4556
4696
  "directive_pending_text": False,
4557
4697
  "related_tasks_raw": "", "related_tasks_pending_text": False,
4558
4698
  "clarification_response_path": "", "clarification_pending_text": False,
4699
+ "selected_direction_path": "",
4559
4700
  "reverify_scope": "", "reverify_scope_pending_text": False,
4560
4701
  "pr_template_path": "", "pr_template_pending_text": False,
4561
4702
  "pr_template_scope": "",
@@ -4924,6 +5065,7 @@ def render_args(state: WizardState) -> dict[str, str]:
4924
5065
  "report-writer-model": state.report_writer_model,
4925
5066
  "related-tasks": state.related_tasks_raw,
4926
5067
  "clarification-response": state.clarification_response_path,
5068
+ "selected-direction": state.selected_direction_path,
4927
5069
  "reverify-scope": (
4928
5070
  state.reverify_scope
4929
5071
  if state.task_type == "implementation-planning" else ""
@@ -5080,6 +5222,8 @@ def confirmation_block(state: WizardState) -> str:
5080
5222
  reverify_line = _reverify_scope_line(state)
5081
5223
  if reverify_line is not None:
5082
5224
  lines.append(reverify_line)
5225
+ if state.selected_direction_path:
5226
+ lines.append(f" selected-direction: {state.selected_direction_path}")
5083
5227
  if state.task_type == "release-handoff" and state.handoff_mode:
5084
5228
  scope = (
5085
5229
  _msg(state.workspace_root, "confirmation",
@@ -51,6 +51,7 @@ FINAL_VERIFICATION_HEADERS = (
51
51
  SUPPORTED_TASK_TYPES = frozenset({
52
52
  "requirements-discovery",
53
53
  "error-analysis",
54
+ "implementation-option-selection",
54
55
  "implementation-planning",
55
56
  "improvement-discovery",
56
57
  "implementation",
@@ -67,6 +68,7 @@ ANALYSIS_DUTY_BY_TASK_TYPE: dict[str, AgentAudience] = {
67
68
  "requirements-discovery": "discovery-worker",
68
69
  "improvement-discovery": "discovery-worker",
69
70
  "error-analysis": "diagnosis-worker",
71
+ "implementation-option-selection": "direction-selection-worker",
70
72
  "implementation-planning": "planning-worker",
71
73
  }
72
74
  WORKER_PREAMBLE_FILENAME_BY_AUDIENCE = {
@@ -17,22 +17,34 @@ from okstra_ctl.analysis_inputs import ANALYSIS_TASK_TYPES
17
17
  PHASE_SEQUENCE = [
18
18
  "requirements-discovery",
19
19
  "error-analysis",
20
+ "implementation-option-selection",
20
21
  "implementation-planning",
21
22
  "implementation",
22
23
  "final-verification",
23
24
  "release-handoff",
24
25
  ]
25
26
 
27
+ REQUIREMENTS_DISCOVERY_ROUTING_TARGETS = frozenset(
28
+ {"error-analysis", "implementation-option-selection"}
29
+ )
30
+
31
+ ERROR_ANALYSIS_ROUTING_DIRECTIONS = {
32
+ "error-analysis": "continue-investigation",
33
+ "implementation-option-selection": "begin-option-selection",
34
+ }
35
+
26
36
  DEFAULT_NEXT_PHASE = {
27
- "requirements-discovery": "pending-routing-decision",
37
+ "requirements-discovery": "implementation-option-selection",
28
38
  "improvement-discovery": "pending-routing-decision",
29
39
  **{task_type: "pending-routing-decision" for task_type in ANALYSIS_TASK_TYPES},
30
- "error-analysis": "implementation-planning",
40
+ "error-analysis": "implementation-option-selection",
41
+ "implementation-option-selection": "implementation-planning",
31
42
  "implementation-planning": "implementation",
32
43
  "implementation": "final-verification",
33
44
  # final-verification 의 다음 단계는 verdict 에 따라 갈리므로 정적 매핑은
34
45
  # `pending-release-handoff` 로 둔다 (accepted 일 때만 release-handoff 로
35
- # 진입; 그 외에는 error-analysis / implementation-planning 으로 리라우팅).
46
+ # 진입; 그 외에는 error-analysis / implementation-option-selection /
47
+ # implementation-planning 으로 리라우팅).
36
48
  "final-verification": "pending-release-handoff",
37
49
  "release-handoff": "done-or-follow-up",
38
50
  }
@@ -87,12 +99,22 @@ PHASE_RULES: dict[str, dict[str, str]] = {
87
99
  " - one endStateCoverage row per brief end-state id (this phase authors no goal of its own)"
88
100
  ),
89
101
  },
102
+ "implementation-option-selection": {
103
+ "allowed": (
104
+ " - candidate-comparison evidence for up to three options per worker\n"
105
+ " - preselected-validation that re-evaluates the merged candidate set\n"
106
+ " - a ranked display of at most three options with requirement mappings\n"
107
+ " - an audit record for every rejected candidate\n"
108
+ " - one endStateCoverage row per brief end-state id (this phase authors no goal of its own)"
109
+ ),
110
+ },
90
111
  "implementation-planning": {
91
112
  "allowed": (
92
113
  " - pre-planning context exploration notes (files/interfaces inspected, recent commits scanned, ambiguities flagged)\n"
93
- " - at least two implementation option candidates, each with a File Structure list (Create/Modify/Delete with one-line responsibility per file), affected interfaces, and blast-radius estimate\n"
94
- " - trade-off matrix across options (complexity, risk, reversibility, test cost, rollout cost) and recommended option with rationale tied to isolation / single-responsibility / YAGNI principles\n"
95
- " - bite-sized stepwise execution order for the recommended option (each step ~2-5 min, exact file paths and commands, TDD ordering when applicable, no placeholders)\n"
114
+ " - selected-direction planning: realize the one validated selected direction without reopening candidate comparison, with a File Structure list (Create/Modify/Delete with one-line responsibility per file), affected interfaces, and blast-radius estimate\n"
115
+ " - legacy candidate-comparison planning only: at least two implementation option candidates, each with a File Structure list (Create/Modify/Delete with one-line responsibility per file), affected interfaces, and blast-radius estimate\n"
116
+ " - legacy candidate-comparison planning only: a trade-off matrix across options (complexity, risk, reversibility, test cost, rollout cost) and recommended option with rationale tied to isolation / single-responsibility / YAGNI principles\n"
117
+ " - bite-sized stepwise execution order for the selected direction or legacy recommended option (each step ~2-5 min, exact file paths and commands, TDD ordering when applicable, no placeholders)\n"
96
118
  " - dependency / migration risk assessment, validation checklist (pre / mid / post with exact commands), rollback strategy with revert path and trigger signal\n"
97
119
  " - every unresolved ambiguity registered as a `Blocks=approval` row in the `## 1. Clarification Items` table (do NOT create a separate `Open Questions` block under `5.5.x` — the unified table is the single home)\n"
98
120
  " - YAML frontmatter line `approved: false` awaiting human flip to `true`\n"
@@ -117,7 +139,7 @@ PHASE_RULES: dict[str, dict[str, str]] = {
117
139
  "allowed": (
118
140
  " - acceptance verdict with requirement coverage assessment\n"
119
141
  " - residual risk and regression notes\n"
120
- " - recommended follow-up routing (`error-analysis` / `implementation-planning` / `release-handoff`) for any defects detected"
142
+ " - recommended follow-up routing (`error-analysis` / `implementation-option-selection` / `implementation-planning` / `release-handoff`) for any defects detected"
121
143
  ),
122
144
  },
123
145
  "release-handoff": {