okstra 0.225.0 → 0.226.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 (75) hide show
  1. package/dist/cli-registry.mjs +6 -0
  2. package/dist/cli-registry.mjs.map +1 -1
  3. package/dist/commands/execute/render-bundle.mjs +5 -0
  4. package/dist/commands/execute/render-bundle.mjs.map +1 -1
  5. package/dist/commands/pr/pr.mjs +42 -13
  6. package/dist/commands/pr/pr.mjs.map +1 -1
  7. package/docs/cli.md +19 -3
  8. package/docs/project-structure-overview.md +2 -0
  9. package/package.json +1 -1
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/bin/lib/okstra/cli.sh +5 -1
  12. package/runtime/bin/lib/okstra/globals.sh +1 -0
  13. package/runtime/bin/lib/okstra/usage.sh +3 -0
  14. package/runtime/bin/okstra.sh +2 -0
  15. package/runtime/prompts/duties/code-reviewer.json +7 -4
  16. package/runtime/prompts/duties/technical-verification-worker.json +2 -1
  17. package/runtime/prompts/lead/convergence.md +4 -1
  18. package/runtime/prompts/lead/phase-routing.md +7 -3
  19. package/runtime/prompts/lead/team-contract.md +1 -1
  20. package/runtime/prompts/wizard/prompts.ko.json +15 -0
  21. package/runtime/python/okstra_ctl/adapters/providers/codex/adapter.py +5 -0
  22. package/runtime/python/okstra_ctl/analysis_packet.py +108 -2
  23. package/runtime/python/okstra_ctl/approved_plan_stage_extract.py +206 -0
  24. package/runtime/python/okstra_ctl/brief_sources.py +200 -0
  25. package/runtime/python/okstra_ctl/business_flow/store.py +15 -2
  26. package/runtime/python/okstra_ctl/cmux.py +22 -5
  27. package/runtime/python/okstra_ctl/code_review_coverage.py +26 -2
  28. package/runtime/python/okstra_ctl/convergence_critic_prompt.py +4 -1
  29. package/runtime/python/okstra_ctl/design_surfaces.py +19 -7
  30. package/runtime/python/okstra_ctl/execution_mutation_audit.py +20 -18
  31. package/runtime/python/okstra_ctl/implementation_direction.py +10 -0
  32. package/runtime/python/okstra_ctl/initial_prompt_materialization.py +41 -0
  33. package/runtime/python/okstra_ctl/manager_split.py +185 -27
  34. package/runtime/python/okstra_ctl/next_phase.py +3 -1
  35. package/runtime/python/okstra_ctl/phases/final_verification/prior_blockers.py +128 -0
  36. package/runtime/python/okstra_ctl/phases/final_verification/profile.md +8 -5
  37. package/runtime/python/okstra_ctl/phases/final_verification/report_assets/final-verification.template.html +1 -1
  38. package/runtime/python/okstra_ctl/phases/final_verification/validation.py +84 -0
  39. package/runtime/python/okstra_ctl/phases/implementation/instructions/_implementation-executor.md +1 -1
  40. package/runtime/python/okstra_ctl/phases/implementation_option_selection/authoring.py +13 -5
  41. package/runtime/python/okstra_ctl/phases/implementation_option_selection/boundary.json +1 -1
  42. package/runtime/python/okstra_ctl/phases/implementation_option_selection/entry.py +67 -0
  43. package/runtime/python/okstra_ctl/phases/implementation_option_selection/profile.md +4 -2
  44. package/runtime/python/okstra_ctl/phases/implementation_option_selection/report_assets/implementation-option-selection.template.html +2 -1
  45. package/runtime/python/okstra_ctl/phases/implementation_option_selection/validation.py +127 -18
  46. package/runtime/python/okstra_ctl/phases/implementation_planning/profile.md +2 -1
  47. package/runtime/python/okstra_ctl/phases/technical_verification/profile.md +2 -2
  48. package/runtime/python/okstra_ctl/report_assembly.py +23 -2
  49. package/runtime/python/okstra_ctl/report_views.py +2 -1
  50. package/runtime/python/okstra_ctl/run.py +46 -1
  51. package/runtime/python/okstra_ctl/run_context.py +13 -8
  52. package/runtime/python/okstra_ctl/wizard/confirmation.py +6 -0
  53. package/runtime/python/okstra_ctl/wizard/ids.py +6 -0
  54. package/runtime/python/okstra_ctl/wizard/registry.py +21 -0
  55. package/runtime/python/okstra_ctl/wizard/render.py +4 -0
  56. package/runtime/python/okstra_ctl/wizard/sources.py +33 -11
  57. package/runtime/python/okstra_ctl/wizard/state.py +4 -0
  58. package/runtime/python/okstra_ctl/wizard/steps_options.py +74 -9
  59. package/runtime/python/okstra_ctl/worker_prompt_body.py +21 -1
  60. package/runtime/python/okstra_ctl/worker_prompt_headers.py +11 -4
  61. package/runtime/python/okstra_ctl/worker_prompt_policy.py +3 -0
  62. package/runtime/schemas/final-report-v2.0.schema.json +74 -0
  63. package/runtime/schemas/final-report-v3.0.schema.json +99 -2
  64. package/runtime/skills/okstra-brief-gen/SKILL.md +26 -0
  65. package/runtime/skills/okstra-code-review/SKILL.md +2 -0
  66. package/runtime/skills/okstra-code-review/references/census-rules.md +16 -2
  67. package/runtime/skills/okstra-code-review/references/review-calibration.md +7 -1
  68. package/runtime/skills/okstra-manager/SKILL.md +15 -2
  69. package/runtime/skills/okstra-pr-gen/SKILL.md +15 -1
  70. package/runtime/templates/reports/brief.template.md +10 -1
  71. package/runtime/templates/reports/html/i18n/en.json +8 -0
  72. package/runtime/templates/reports/html/i18n/ko.json +8 -0
  73. package/runtime/templates/worker-prompt-preamble.md +1 -1
  74. package/runtime/validators/validate-brief.py +39 -1
  75. package/runtime/validators/validate-run.py +5 -0
@@ -55,7 +55,7 @@ from .phases.implementation_planning.entry import (
55
55
  _validate_reverify_scope,
56
56
  )
57
57
  from . import fix_cycles
58
- from .analysis_packet import build_analysis_packet
58
+ from .analysis_packet import build_analysis_packet, cited_documents_block
59
59
  from .mcp_servers import NO_MCP_SERVERS_LINE, prepare_mcp
60
60
  from .coverage_census import (
61
61
  CensusError,
@@ -236,6 +236,7 @@ from .locks import worktree_provision_mutex
236
236
  from . import stage_targets as _stage_targets
237
237
  from .phases.catalog import PhaseAssetError, profile_markdown, resolve_include
238
238
  from .phases.final_verification.target import acquire_final_verification_target
239
+ from .phases.final_verification.prior_blockers import prior_blockers_packet_block
239
240
  from .phases.final_verification.entry import (
240
241
  apply_verification_target,
241
242
  reject_prepare_flags,
@@ -534,6 +535,7 @@ def _set_data_json_approved_true_if_present(path: Path) -> bool:
534
535
  # 가 whole-task 통합 실패를 동일 타입으로 받는다. run.py 는 재노출만 한다.
535
536
  PrepareError = _stage_targets.PrepareError
536
537
  from .phases.implementation_option_selection.entry import (
538
+ selection_mode_packet_block,
537
539
  validate_brief_requirements,
538
540
  )
539
541
 
@@ -607,6 +609,9 @@ class PrepareInputs:
607
609
  clarification_response_path: str = "" # absolute or empty
608
610
  # implementation-planning 신규 실행 전용: 검증된 방향 선택 최종 보고서.
609
611
  selected_direction_path: str = ""
612
+ # implementation-option-selection 전용: 리드가 미리 정한 방향과 그 근거.
613
+ # 값이 있으면 이 run 은 `preselected-validation`, 없으면 `candidate-comparison`.
614
+ preselected_direction: str = ""
610
615
  # implementation-planning 전용: 사용자가 고른 이번 재실행의 재검증 범위.
611
616
  # "" / "auto" = 리드의 `okstra incremental-scope` 판정에 맡김, "full" =
612
617
  # 전체 재검증 강제, "<stage csv>" = 그 stage 들을 impacted 로 지정.
@@ -1136,6 +1141,7 @@ def _canonical_argv(inp: PrepareInputs, ctx: dict) -> list[str]:
1136
1141
  ("--implementation-option", inp.implementation_option),
1137
1142
  ("--clarification-response", inp.clarification_response_path),
1138
1143
  ("--selected-direction", inp.selected_direction_path),
1144
+ ("--preselected-direction", inp.preselected_direction),
1139
1145
  (
1140
1146
  "--lead-provider",
1141
1147
  inp.lead_provider or lead_fallbacks.get("LEAD_PROVIDER", ""),
@@ -1416,6 +1422,11 @@ def _validate_prepare_inputs(project_root: Path, inp: PrepareInputs) -> list:
1416
1422
  raise PrepareError(f"task brief not found: {inp.brief_path}")
1417
1423
  elif inp.task_type == "implementation-option-selection":
1418
1424
  validate_brief_requirements(inp.brief_path)
1425
+ if inp.preselected_direction and inp.task_type != "implementation-option-selection":
1426
+ raise PrepareError(
1427
+ "--preselected-direction is only meaningful with --task-type "
1428
+ f"implementation-option-selection; got {inp.task_type}"
1429
+ )
1419
1430
  ctx_stage_map: list = []
1420
1431
  # implementation 과 final-verification 은 둘 다 승인된 plan 의 Stage Map 을
1421
1432
  # 입력으로 받는다(전자는 실행 scope, 후자는 검증 scope).
@@ -3068,6 +3079,22 @@ def _write_instruction_set_sources(
3068
3079
  if inp.clarification_response_path else ""
3069
3080
  ),
3070
3081
  verification_target_path=ctx.get("VERIFICATION_TARGET_RELATIVE_PATH", ""),
3082
+ selection_mode_text=(
3083
+ selection_mode_packet_block(
3084
+ inp.preselected_direction,
3085
+ Path(inp.clarification_response_path) if inp.clarification_response_path else None,
3086
+ Path(ctx["PROJECT_ROOT"]),
3087
+ )
3088
+ if inp.task_type == "implementation-option-selection" else ""
3089
+ ),
3090
+ cited_documents_text=cited_documents_block(
3091
+ (instruction_set / "task-brief.md").read_text(encoding="utf-8"),
3092
+ Path(ctx["PROJECT_ROOT"]),
3093
+ ),
3094
+ prior_verification_text=(
3095
+ prior_blockers_packet_block(Path(ctx["FINAL_REPORT_PATH"]))
3096
+ if inp.task_type == "final-verification" else ""
3097
+ ),
3071
3098
  )
3072
3099
  if inp.task_type == "technical-verification":
3073
3100
  packet += materialize_technical_input_packet(
@@ -3296,6 +3323,7 @@ def _persist_run_inputs(
3296
3323
  "approvedPlanPath": approved_plan_path,
3297
3324
  "clarificationResponsePath": inp.clarification_response_path,
3298
3325
  "selectedDirectionPath": inp.selected_direction_path,
3326
+ "preselectedDirection": inp.preselected_direction,
3299
3327
  "analysisTarget": json.loads(ctx.get("ANALYSIS_TARGET_JSON", "{}")).get(
3300
3328
  "requestedValue", ""
3301
3329
  ),
@@ -4060,6 +4088,11 @@ def _prepare_user_authorization(inp: PrepareInputs) -> dict:
4060
4088
  for key, value in expected.items():
4061
4089
  if record["scope"].get(key) != value:
4062
4090
  raise PrepareError(f"user authorization scope does not match --{key}; confirm the changed scope")
4091
+ # A record confirmed before this key existed scoped a run without a preselected direction.
4092
+ if record["scope"].get("preselected-direction", "") != inp.preselected_direction:
4093
+ raise PrepareError(
4094
+ "user authorization scope does not match --preselected-direction; confirm the changed scope"
4095
+ )
4063
4096
  for key, path in (
4064
4097
  ("task-brief", "" if inp.task_type in ("release-handoff", "discussion") else inp.brief_path),
4065
4098
  ("approved-plan", inp.approved_plan_path),
@@ -4537,6 +4570,7 @@ Usage:
4537
4570
  [--related-tasks <list>] [--base-ref <ref>] \
4538
4571
  [--clarification-response <path>] [--work-category <cat>] \
4539
4572
  [--selected-direction <selection-final-report.md>] \
4573
+ [--preselected-direction <direction and citation>] \
4540
4574
  [--stage <auto|N>] [--stages <csv>] [--pr-template-path <path>] \
4541
4575
  [--fix-cycle <yes|no>] [--discussion-start <link|text|task-key>]
4542
4576
 
@@ -4671,6 +4705,16 @@ def build_prepare_argument_parser():
4671
4705
  )
4672
4706
  p.add_argument("--clarification-response", default="", dest="clarification_response_path")
4673
4707
  p.add_argument("--selected-direction", default="", dest="selected_direction_path")
4708
+ p.add_argument(
4709
+ "--preselected-direction",
4710
+ default="",
4711
+ dest="preselected_direction",
4712
+ help=(
4713
+ "implementation-option-selection only: the one direction this run "
4714
+ "validates, with its citation. Sets the run to preselected-validation; "
4715
+ "omitted means candidate-comparison."
4716
+ ),
4717
+ )
4674
4718
  p.add_argument(
4675
4719
  "--reverify-scope",
4676
4720
  default="",
@@ -4907,6 +4951,7 @@ def main(argv: list[str]) -> int:
4907
4951
  stages=args.stages,
4908
4952
  clarification_response_path=clarification_abs,
4909
4953
  selected_direction_path=selected_direction_abs,
4954
+ preselected_direction=args.preselected_direction.strip(),
4910
4955
  reverify_scope=args.reverify_scope,
4911
4956
  pr_template_path=args.pr_template_path,
4912
4957
  render_only=args.render_only,
@@ -36,12 +36,15 @@ def latest_run_inputs(
36
36
  task_id: str,
37
37
  *,
38
38
  phase_segment: str = "",
39
+ stage: Optional[int] = None,
39
40
  ) -> dict:
40
41
  """가장 최근 ``run-inputs-*.json`` 의 ``inputs`` dict 를 반환 (부재 시 {}).
41
42
 
42
- ``phase_segment`` 가 주어지면 그 phase 의 ``runs/<seg>/manifests/`` 만 스캔한다
43
- (resume 는 같은 phase 를 재실행하므로 그 phase 의 직전 입력이 기준). 비어 있으면
44
- task 의 모든 phase 를 통틀어 가장 최근(mtime) 파일을 고른다.
43
+ ``phase_segment`` 가 주어지면 그 phase 의 ``runs/<seg>/manifests/`` 와
44
+ ``runs/<seg>/stage-*/manifests/`` 만 스캔한다 (resume 는 같은 phase 를 재실행하므로
45
+ 그 phase 의 직전 입력이 기준). 비어 있으면 task 의 모든 phase 를 통틀어 가장
46
+ 최근(mtime) 파일을 고른다. ``stage`` 는 ``phase_segment`` 와 함께 써서
47
+ ``runs/<seg>/stage-<N>/manifests/`` 하나로 좁힌다.
45
48
 
46
49
  페이로드 구조는 ``{"schemaVersion", "writtenAt", "inputs": {...}}`` 이며 실제
47
50
  입력은 top-level 이 아니라 ``inputs`` 아래에 있다. run-inputs 의 위치/구조 지식은
@@ -52,13 +55,15 @@ def latest_run_inputs(
52
55
  runs_base = task_runs_dir(Path(project_root), task_group, task_id)
53
56
  if not runs_base.is_dir():
54
57
  return {}
55
- if phase_segment:
56
- glob_iter = (runs_base / phase_segment / "manifests").glob(
57
- "run-inputs-*.json")
58
+ if phase_segment and stage is not None:
59
+ patterns = [f"{phase_segment}/stage-{int(stage)}/manifests/run-inputs-*.json"]
58
60
  else:
59
- glob_iter = runs_base.glob("*/manifests/run-inputs-*.json")
61
+ # implementation / final-verification run 은 stage-<N>/ 한 단계 아래에 있다.
62
+ scope = phase_segment or "*"
63
+ patterns = [f"{scope}/manifests/run-inputs-*.json",
64
+ f"{scope}/stage-*/manifests/run-inputs-*.json"]
60
65
  candidates: list[tuple[float, Path]] = []
61
- for inp in glob_iter:
66
+ for inp in (path for pattern in patterns for path in runs_base.glob(pattern)):
62
67
  try:
63
68
  candidates.append((inp.stat().st_mtime, inp))
64
69
  except OSError:
@@ -7,6 +7,7 @@ from typing import Optional
7
7
  from okstra_ctl.registry.host_registry import default_host_registry
8
8
  from okstra_ctl.clarification_items import sidecar_answers, user_response_sidecars
9
9
  from okstra_ctl.incremental_scope import CARRY_ALL_SCOPE
10
+ from okstra_ctl.phases.implementation_option_selection.entry import selection_mode
10
11
  from okstra_ctl.wizard_stage_intent import wizard_stage_confirmation_label, resolve_wizard_stage_intent
11
12
  from okstra_ctl.worktree import (
12
13
  compute_worktree_path,
@@ -325,6 +326,11 @@ def confirmation_block(state: WizardState) -> str:
325
326
  lines.append(reverify_line)
326
327
  if state.selected_direction_path:
327
328
  lines.append(f" selected-direction: {state.selected_direction_path}")
329
+ if state.task_type == "implementation-option-selection":
330
+ mode = selection_mode(state.preselected_direction)
331
+ lines.append(f" selection-mode: {mode}")
332
+ if state.preselected_direction:
333
+ lines.append(f" preselected-direction: {state.preselected_direction}")
328
334
  if state.task_type == "release-handoff" and state.handoff_stages:
329
335
  scope = _msg(state.workspace_root, "confirmation",
330
336
  "handoff_scope_stages", stages=state.handoff_stages)
@@ -220,6 +220,12 @@ S_CLARIFICATION_PICK = "clarification_pick"
220
220
  S_CLARIFICATION = "clarification"
221
221
 
222
222
 
223
+ S_SELECTION_MODE_PICK = "selection_mode_pick"
224
+
225
+
226
+ S_PRESELECTED_DIRECTION = "preselected_direction"
227
+
228
+
223
229
  S_REVERIFY_SCOPE_PICK = "reverify_scope_pick"
224
230
 
225
231
 
@@ -56,6 +56,7 @@ from .ids import (
56
56
  S_HANDOFF_STAGE_PICK,
57
57
  S_PROJECT_EVIDENCE,
58
58
  S_PROJECT_EVIDENCE_PICK,
59
+ S_PRESELECTED_DIRECTION,
59
60
  S_PR_TEMPLATE,
60
61
  S_PR_TEMPLATE_PICK,
61
62
  S_REASONING_EFFORT,
@@ -67,6 +68,7 @@ from .ids import (
67
68
  S_REVERIFY_SCOPE_PICK,
68
69
  S_REVERIFY_SCOPE_STAGES,
69
70
  S_SELECTED_DIRECTION_PICK,
71
+ S_SELECTION_MODE_PICK,
70
72
  S_STAGE_PICK,
71
73
  S_TASK_GROUP,
72
74
  S_TASK_GROUP_TEXT,
@@ -185,6 +187,8 @@ from .steps_options import (
185
187
  _build_pr_template_scope,
186
188
  _build_related_tasks,
187
189
  _build_related_tasks_pick,
190
+ _build_preselected_direction,
191
+ _build_selection_mode_pick,
188
192
  _submit_clarification,
189
193
  _submit_clarification_pick,
190
194
  _submit_directive,
@@ -194,6 +198,8 @@ from .steps_options import (
194
198
  _submit_pr_template_scope,
195
199
  _submit_related_tasks,
196
200
  _submit_related_tasks_pick,
201
+ _submit_preselected_direction,
202
+ _submit_selection_mode_pick,
197
203
  )
198
204
  from .steps_roles import _build_reuse_previous, _submit_reuse_previous
199
205
  from .confirmation import _build_confirm, _submit_confirm
@@ -556,6 +562,19 @@ STEPS: list[Step] = [
556
562
  and S_CLARIFICATION not in s.answered),
557
563
  build=_build_clarification, submit=_submit_clarification,
558
564
  owns=("clarification_response_path", "clarification_pending_text")),
565
+ # clarification 뒤 — 이어받는 리포트의 다음 단계 지시를 라벨에 보여 준다.
566
+ Step(S_SELECTION_MODE_PICK,
567
+ applies=lambda s: (s.task_type == "implementation-option-selection"
568
+ and _identity_ready(s)
569
+ and not s.clarification_pending_text
570
+ and S_SELECTION_MODE_PICK not in s.answered),
571
+ build=_build_selection_mode_pick, submit=_submit_selection_mode_pick,
572
+ owns=("preselected_direction", "preselected_direction_pending_text")),
573
+ Step(S_PRESELECTED_DIRECTION,
574
+ applies=lambda s: (s.preselected_direction_pending_text
575
+ and S_PRESELECTED_DIRECTION not in s.answered),
576
+ build=_build_preselected_direction, submit=_submit_preselected_direction,
577
+ owns=("preselected_direction", "preselected_direction_pending_text")),
559
578
  # clarification 뒤에 온다. `_planning_rerun_selected` 는 clarification 답을 읽어
560
579
  # "이 런은 기존 계획서의 재실행" 인지 판정하는데, 이 단계가 clarification 앞에
561
580
  # 있던 동안 그 값은 언제나 비어 있었다 — 가드가 참이 될 수 없는 자리에 놓여
@@ -663,6 +682,8 @@ def _ready_for_confirm(s: WizardState) -> bool:
663
682
  S_RELATED_TASKS,
664
683
  S_CLARIFICATION_PICK,
665
684
  S_CLARIFICATION,
685
+ S_SELECTION_MODE_PICK,
686
+ S_PRESELECTED_DIRECTION,
666
687
  S_PR_TEMPLATE_PICK,
667
688
  S_PR_TEMPLATE,
668
689
  S_PR_TEMPLATE_SCOPE,
@@ -112,6 +112,10 @@ def render_args(state: WizardState) -> dict[str, Any]:
112
112
  "related-tasks": state.related_tasks_raw,
113
113
  "clarification-response": state.clarification_response_path,
114
114
  "selected-direction": state.selected_direction_path,
115
+ "preselected-direction": (
116
+ state.preselected_direction
117
+ if state.task_type == "implementation-option-selection" else ""
118
+ ),
115
119
  "reverify-scope": (
116
120
  state.reverify_scope
117
121
  if state.task_type == "implementation-planning" else ""
@@ -25,6 +25,7 @@ from okstra_ctl.final_report_paths import final_report_data_path
25
25
  from okstra_ctl.plan_run_root import list_implementation_planning_reports
26
26
  from okstra_ctl.stage_map import StageMapError, parse_stage_map_file
27
27
  from okstra_ctl.workflow import PHASE_SEQUENCE
28
+ from okstra_ctl.wizard_stage_intent import WHOLE_TASK_STAGE
28
29
  from okstra_ctl import worktree_registry
29
30
  from okstra_ctl.worktree import is_git_work_tree, main_worktree_path
30
31
  from okstra_ctl.paths import RunRef, task_runs_dir
@@ -362,14 +363,32 @@ def _contained_final_report_data_path(report: Path, task_root: Path) -> Path:
362
363
  )
363
364
 
364
365
 
366
+ def _stage_scoped_report_patterns(
367
+ segment: str, selected_stage: str
368
+ ) -> tuple[str, ...]:
369
+ """`segment` 아래에서 이 run 의 stage 범위에 속한 보고서 glob.
370
+
371
+ 숫자 stage 는 `stage-<N>/` 만, 전체 작업(flat run 디렉터리)은 flat 만 본다 —
372
+ 다른 범위의 답변은 이 run 의 clarification 이 아니다. stage 를 고르지 않는
373
+ run(release-handoff 등)은 둘 다 본다.
374
+ """
375
+ report = "reports/final-report-*.data.json"
376
+ if selected_stage.isdigit():
377
+ return (f"{segment}/stage-{selected_stage}/{report}",)
378
+ if selected_stage == WHOLE_TASK_STAGE:
379
+ return (f"{segment}/{report}",)
380
+ return (f"{segment}/{report}", f"{segment}/stage-*/{report}")
381
+
382
+
365
383
  def _newest_contained_final_report(
366
384
  runs_base: Path,
367
385
  task_root: Path,
368
- glob_pattern: str,
386
+ glob_patterns: tuple[str, ...],
369
387
  project_root: Path,
370
388
  ) -> Path | None:
371
389
  candidates: list[Path] = []
372
- for report in runs_base.glob(glob_pattern):
390
+ reports = (path for pattern in glob_patterns for path in runs_base.glob(pattern))
391
+ for report in reports:
373
392
  if not report.is_file():
374
393
  continue
375
394
  try:
@@ -513,7 +532,7 @@ def _technical_evidence_for_comparison(
513
532
  project_root = Path(state.project_root)
514
533
  report = _newest_contained_final_report(
515
534
  task_root / "runs", task_root,
516
- "technical-verification/reports/final-report-*.data.json", project_root,
535
+ ("technical-verification/reports/final-report-*.data.json",), project_root,
517
536
  )
518
537
  if report is None:
519
538
  return source
@@ -531,10 +550,11 @@ def _suggest_latest_final_report(state: WizardState) -> str:
531
550
  """clarification carry-in 으로 추천할 직전 final-report 의 relpath.
532
551
 
533
552
  resume-clarification 은 "같은 phase 를 답변과 함께 재실행" 이므로 재실행
534
- task-type **자신의** 보고서(``runs/<task-type>/reports/final-report-*.md``)를
535
- 우선한다. 그 phase 에 보고서가 없을 때만(예: 다음 phase 로 진행) 전체 phase 의
536
- mtime 최신으로 폴백한다 — 과거엔 무조건 전체 mtime 최신이라 더 최근에 재렌더된
537
- 다른 phase 보고서를 잘못 집을 수 있었다. 못 찾으면 빈 문자열.
553
+ task-type **자신의** 보고서(``runs/<task-type>[/stage-<N>]/reports/``)를
554
+ 우선한다(stage 범위는 ``_stage_scoped_report_patterns``). 그 phase 에 보고서가
555
+ 없을 때만(예: 다음 phase 로 진행) 전체 phase 의 mtime 최신으로 폴백한다 —
556
+ 과거엔 무조건 전체 mtime 최신이라 더 최근에 재렌더된 다른 phase 보고서를 잘못
557
+ 집을 수 있었다. 못 찾으면 빈 문자열.
538
558
  """
539
559
  if not state.task_group or not state.task_id or not state.project_root:
540
560
  return ""
@@ -560,7 +580,7 @@ def _suggest_latest_final_report(state: WizardState) -> str:
560
580
  best = _newest_contained_final_report(
561
581
  runs_base,
562
582
  task_root,
563
- f"{seg}/reports/final-report-*.data.json",
583
+ _stage_scoped_report_patterns(seg, state.selected_stage),
564
584
  Path(state.project_root),
565
585
  )
566
586
  # implementation-planning 의 `--clarification-response` 는 같은 phase 의
@@ -572,7 +592,9 @@ def _suggest_latest_final_report(state: WizardState) -> str:
572
592
  best = _newest_contained_final_report(
573
593
  runs_base,
574
594
  task_root,
575
- "*/reports/final-report-*.data.json",
595
+ # 다른 phase 의 flat 보고서(계획서 등)는 stage 범위와 무관하게 후보다.
596
+ ("*/reports/final-report-*.data.json",
597
+ *_stage_scoped_report_patterns("*", state.selected_stage)),
576
598
  Path(state.project_root),
577
599
  )
578
600
  best = _technical_evidence_for_comparison(state, best, task_root)
@@ -592,12 +614,12 @@ def _suggest_latest_final_report(state: WizardState) -> str:
592
614
 
593
615
 
594
616
  def _latest_run_inputs(
595
- state: WizardState, *, phase_segment: str = ""
617
+ state: WizardState, *, phase_segment: str = "", stage: Optional[int] = None
596
618
  ) -> dict:
597
619
  """직전 run 의 입력 스냅샷. 위치/구조 지식은 run_context.latest_run_inputs 가 SSOT."""
598
620
  return latest_run_inputs(
599
621
  state.project_root, state.task_group, state.task_id,
600
- phase_segment=phase_segment)
622
+ phase_segment=phase_segment, stage=stage)
601
623
 
602
624
 
603
625
  def _clarification_pick_required(state: WizardState) -> bool:
@@ -120,6 +120,9 @@ class WizardState:
120
120
  clarification_pending_text: bool = False
121
121
  last_final_report_cached: str = ""
122
122
  selected_direction_path: str = ""
123
+ # implementation-option-selection: 리드가 미리 정한 방향과 근거. 비면 candidate-comparison.
124
+ preselected_direction: str = ""
125
+ preselected_direction_pending_text: bool = False
123
126
  # "" | "auto" | "full" | "<stage csv>" — 사용자가 고른 이번 재실행의 재검증
124
127
  # 범위. implementation-planning 재실행에서 좁힐 여지가 있을 때만 채워진다.
125
128
  reverify_scope: str = ""
@@ -625,6 +628,7 @@ _FIELD_DEFAULTS: dict[str, Any] = {
625
628
  "related_tasks_raw": "", "related_tasks_pending_text": False,
626
629
  "clarification_response_path": "", "clarification_pending_text": False,
627
630
  "selected_direction_path": "",
631
+ "preselected_direction": "", "preselected_direction_pending_text": False,
628
632
  "reverify_scope": "", "reverify_scope_pending_text": False,
629
633
  "pr_template_path": "", "pr_template_pending_text": False,
630
634
  "pr_template_scope": "",
@@ -8,10 +8,17 @@ from typing import Callable, Optional
8
8
 
9
9
  from okstra_ctl.clarification_items import user_response_sidecars
10
10
  from okstra_ctl.domain.worker_exec import REASONING_EFFORTS
11
+ from okstra_ctl.ids import slugify_task_segment
11
12
  from okstra_ctl.manager_paths import MANAGER_CONTEXT_DIRECTIVE_PREFIX
12
13
  from okstra_ctl.json_boundary import JsonBoundaryError, load_owned_object, write_owned_object_atomic
13
14
  from okstra_ctl.report_language import is_language_tag
15
+ from okstra_ctl.phases.implementation_option_selection.entry import (
16
+ CANDIDATE_COMPARISON,
17
+ PRESELECTED_VALIDATION,
18
+ upstream_next_steps,
19
+ )
14
20
  from okstra_ctl.phases.implementation_planning.entry import is_existing_planning_report
21
+ from okstra_ctl.prepare_error import PrepareError
15
22
  from okstra_ctl.pr_template import PrTemplateError, resolve_pr_template_path
16
23
  from okstra_project.dirs import project_json_path
17
24
  from okstra_project.state import StateError, list_project_tasks
@@ -23,6 +30,7 @@ from .ids import (
23
30
  S_CLARIFICATION_PICK,
24
31
  S_DIRECTIVE,
25
32
  S_DIRECTIVE_PICK,
33
+ S_PRESELECTED_DIRECTION,
26
34
  S_PR_TEMPLATE,
27
35
  S_PR_TEMPLATE_PICK,
28
36
  S_PR_TEMPLATE_SCOPE,
@@ -30,6 +38,7 @@ from .ids import (
30
38
  S_RELATED_TASKS,
31
39
  S_RELATED_TASKS_PICK,
32
40
  S_REPORT_LANGUAGE,
41
+ S_SELECTION_MODE_PICK,
33
42
  _LATEST_REPORT_TOKEN,
34
43
  _PROJECT_DEFAULT_TOKEN,
35
44
  _REUSE_LAST_TOKEN,
@@ -97,7 +106,13 @@ def _clarification_sidecar_note(state: WizardState, suggestion: str) -> str:
97
106
 
98
107
  def _suggest_last_directive(state: WizardState) -> str:
99
108
  """같은 task 의 가장 최근 run-inputs-*.json 에서 directive 값을 자동 추출."""
100
- val = _latest_run_inputs(state).get("directive") or ""
109
+ inputs: dict = {}
110
+ if state.task_type and state.selected_stage.isdigit():
111
+ # stage run 의 directive 는 그 stage 범위로 쓰였다 — 형제 stage 것은 남의 것이다.
112
+ inputs = _latest_run_inputs(
113
+ state, phase_segment=slugify_task_segment(state.task_type),
114
+ stage=int(state.selected_stage))
115
+ val = (inputs or _latest_run_inputs(state)).get("directive") or ""
101
116
  return val if isinstance(val, str) else ""
102
117
 
103
118
 
@@ -129,9 +144,9 @@ class _OptionalCachedPickSpec:
129
144
  # 추천 옵션 라벨·echo 뒤에 덧붙일 동적 안내(예: 함께 첨부될 사이드카 개수).
130
145
  # (state, 추천 relpath) -> note. 미설정 picker 는 영향 없음.
131
146
  recommend_note: Optional[Callable[["WizardState", str], str]] = None
132
- # Whether the wizard takes the suggestion without asking. A previous directive
133
- # is an instruction written for another run, so reusing it stays a question.
134
- auto_take_suggestion: bool = True
147
+ # Whether the screen is always shown. No recorded value tells the wizard that a
148
+ # run needs no directive, and a previous one was written for another run.
149
+ always_ask: bool = False
135
150
 
136
151
 
137
152
  def _build_optional_cached_pick(state: WizardState, spec: _OptionalCachedPickSpec) -> Prompt:
@@ -149,12 +164,12 @@ def _build_optional_cached_pick(state: WizardState, spec: _OptionalCachedPickSpe
149
164
  setattr(state, spec.cache_attr, suggestion)
150
165
  options.append(_opt(PICK_SKIP, t["options"][PICK_SKIP]))
151
166
  options.append(_opt(PICK_TYPE_CUSTOM, t["options"][PICK_TYPE_CUSTOM]))
152
- if not suggestion:
167
+ if spec.always_ask:
168
+ auto_value = ""
169
+ elif not suggestion:
153
170
  auto_value = PICK_SKIP
154
- elif spec.auto_take_suggestion:
155
- auto_value = spec.recommend_token
156
171
  else:
157
- auto_value = ""
172
+ auto_value = spec.recommend_token
158
173
  return Prompt(
159
174
  step=spec.step, kind="pick",
160
175
  label=t["label"], options=options,
@@ -218,7 +233,7 @@ _DIRECTIVE_PICK_SPEC = _OptionalCachedPickSpec(
218
233
  cache_attr="last_directive_cached", target_attr="directive",
219
234
  pending_attr="directive_pending_text",
220
235
  recommend_note=_directive_context_note,
221
- auto_take_suggestion=False,
236
+ always_ask=True,
222
237
  )
223
238
 
224
239
 
@@ -393,6 +408,56 @@ def _submit_clarification(state: WizardState, value: str) -> Optional[str]:
393
408
  return f"clarification: {p}"
394
409
 
395
410
 
411
+ def _build_selection_mode_pick(state: WizardState) -> Prompt:
412
+ """The mode is the lead's decision; the upstream report's routing is shown beside it."""
413
+ t = _p(state.workspace_root, "selection_mode_pick")
414
+ label = t["label"]
415
+ if state.clarification_response_path:
416
+ try:
417
+ steps = upstream_next_steps(Path(state.clarification_response_path))
418
+ except PrepareError as exc:
419
+ raise WizardError(str(exc)) from exc
420
+ if steps:
421
+ label += t["labels"]["upstream"].format(steps=" / ".join(steps))
422
+ return Prompt(
423
+ step=S_SELECTION_MODE_PICK, kind="pick", label=label,
424
+ options=[_opt(value, text) for value, text in t["options"].items()],
425
+ echo_template=t["echo_template"],
426
+ )
427
+
428
+
429
+ def _submit_selection_mode_pick(state: WizardState, value: str) -> Optional[str]:
430
+ if value == CANDIDATE_COMPARISON:
431
+ state.preselected_direction = ""
432
+ state.preselected_direction_pending_text = False
433
+ return f"selection-mode: {CANDIDATE_COMPARISON}"
434
+ if value == PRESELECTED_VALIDATION:
435
+ state.preselected_direction_pending_text = True
436
+ return None
437
+ raise WizardError(
438
+ f"expected {CANDIDATE_COMPARISON!r} or {PRESELECTED_VALIDATION!r}, got: {value!r}"
439
+ )
440
+
441
+
442
+ def _build_preselected_direction(state: WizardState) -> Prompt:
443
+ t = _p(state.workspace_root, "preselected_direction")
444
+ return Prompt(
445
+ step=S_PRESELECTED_DIRECTION, kind="text",
446
+ label=t["label"], echo_template=t["echo_template"],
447
+ )
448
+
449
+
450
+ def _submit_preselected_direction(state: WizardState, value: str) -> Optional[str]:
451
+ direction = (value or "").strip()
452
+ if not direction:
453
+ raise WizardError(
454
+ f"{PRESELECTED_VALIDATION} needs the direction and the citation that fixed it"
455
+ )
456
+ state.preselected_direction = direction
457
+ state.preselected_direction_pending_text = False
458
+ return f"preselected-direction: {direction}"
459
+
460
+
396
461
  def _build_pr_template(state: WizardState) -> Prompt:
397
462
  t = _p(state.workspace_root, "pr_template")
398
463
  return Prompt(
@@ -24,6 +24,7 @@ def analysis_prompt_body(
24
24
  plan: PromptPlan,
25
25
  approved_plan_path: str = "",
26
26
  selected_direction_path: str = "",
27
+ stage_extract_path: str = "",
27
28
  ) -> list[str]:
28
29
  """Render the provider-neutral body for an initial analysis audience."""
29
30
  label = analysis_worker_label(worker_id)
@@ -43,7 +44,12 @@ def analysis_prompt_body(
43
44
  "",
44
45
  "## Inputs",
45
46
  *analysis_input_lines(
46
- manifest, active_context, plan, approved_plan_path, selected_direction_path
47
+ manifest,
48
+ active_context,
49
+ plan,
50
+ approved_plan_path,
51
+ selected_direction_path,
52
+ stage_extract_path,
47
53
  ),
48
54
  "",
49
55
  mcp_pointer_line(),
@@ -60,7 +66,21 @@ def analysis_input_lines(
60
66
  plan: PromptPlan,
61
67
  approved_plan_path: str = "",
62
68
  selected_direction_path: str = "",
69
+ stage_extract_path: str = "",
63
70
  ) -> list[str]:
71
+ if plan.audience == "implementation-executor":
72
+ # The packet already carries the brief extract, the reference
73
+ # expectations, the profile prelude and the directive; the task brief,
74
+ # analysis material, profile and reference files repeat them in full.
75
+ # Measured 2026-10-30 (dev-11562): those four files were 1,929 lines
76
+ # beside a 4,865-line packet. The clarification file stays: the
77
+ # packet keeps only its extract, and a lazy-delivery prompt has no
78
+ # inlined authority block.
79
+ return existing_input_lines((
80
+ ("Primary analysis packet", instruction_path(manifest, active_context, "analysisPacketPath")),
81
+ ("Approved plan stage extract", stage_extract_path),
82
+ ("Clarification response", instruction_path(manifest, active_context, "clarificationResponsePath")),
83
+ ))
64
84
  if plan.packet_only:
65
85
  packet_path = instruction_path(manifest, active_context, "analysisPacketPath")
66
86
  # 승인 계획은 `## Inputs` 에 실어야 한다. 워커의 읽기 범위는 이 절과
@@ -27,6 +27,8 @@ class WorkerPromptHeaderError(Exception):
27
27
  # skill catalogs, and similar non-okstra artifacts. Restating the scope inline
28
28
  # puts the rule in front of the worker before its first tool call, which is the
29
29
  # only point at which it can still win over those host instructions.
30
+ # 패킷의 페이지 읽기 규칙도 같은 이유로 여기 있다 — 패킷 안 목차는 파일을 연 뒤에야
31
+ # 보이는데, 통째 `cat` 은 그 전에 잘린다(실측 609KB 패킷, codex).
30
32
  READ_SCOPE_HEADER = (
31
33
  "**Read scope:** Read only the paths this prompt enumerates "
32
34
  "(`[Required reading]`, `## Inputs`, verification-target paths) plus "
@@ -39,7 +41,11 @@ READ_SCOPE_HEADER = (
39
41
  "NOT apply inside an okstra worker run: do not auto-read `graphify-out/`, "
40
42
  "`SKILL.md`, or other artifacts outside `<PROJECT_ROOT>/.okstra/`. If an "
41
43
  "un-enumerated file seems essential, record it under *Missing Information "
42
- "or Assumptions* instead of reading it."
44
+ "or Assumptions* instead of reading it. Read the primary analysis packet "
45
+ "in pages of at most 200 lines (`sed -n '1,200p'`, then `'201,400p'`, …), "
46
+ "never with one whole-file `cat` or view: worker CLIs cut long output "
47
+ "without saying so, and the packet's `## Section Index` gives each "
48
+ "section's line range."
43
49
  )
44
50
  EVIDENCE_LEDGER_HEADER = "**Evidence ledger:** required-v1"
45
51
 
@@ -63,12 +69,13 @@ EVIDENCE_CITATION_HEADER = (
63
69
  # 구현 워커는 명령 행을 쓰지 못한다.
64
70
  EVIDENCE_COMMANDS_HEADER = (
65
71
  "**Evidence commands:** Append one `- Evidence command: "
66
- "{\"command\":\"<exact command>\",\"cwd\":\"<project-root>\","
72
+ "{\"command\":\"<exact command>\",\"cwd\":\"<directory the command ran in>\","
67
73
  "\"exitCode\":0,\"outputSummary\":\"<one-line result>\"}` row to the audit "
68
74
  "sidecar for each command that produced or verified a conclusion. Do not "
69
75
  "record exploratory `rg`, `ls`, or file-opening commands. Write the command "
70
- "as you ran it. A value that must not be written down is yours to omit or "
71
- "pass as a `$VAR` reference."
76
+ "as you ran it. When it starts with `cd <dir> &&`, `cwd` is that `<dir>`, "
77
+ "not the shell's starting directory. A value that must not be written down "
78
+ "is yours to omit or pass as a `$VAR` reference."
72
79
  )
73
80
 
74
81
  # `agy`'s write tool validates the target against the Gemini artifact store
@@ -34,6 +34,9 @@ ERRORS_PATH_HEADERS = (
34
34
  # itself: the lead's launch prompt carries them, and no worker reads that.
35
35
  APPROVED_PLAN_HEADER = "**Approved plan:**"
36
36
  IMPLEMENTATION_STAGE_HEADER = "**Stage for this implementation run:**"
37
+ # Executor-only and optional: a prompt published before it, or a run whose plan
38
+ # is a Markdown report, has no extract and the executor reads the plan itself.
39
+ APPROVED_PLAN_STAGE_EXTRACT_HEADER = "**Approved plan stage extract:**"
37
40
  # A stage's verifiers share one worktree and run as one batch. The Tier 3 script
38
41
  # (builds, fixed ports) and the self-mock mutation probe (edits sources in place)
39
42
  # must each run once, by the verifier named here.