okstra 0.102.1 → 0.102.3

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 (162) hide show
  1. package/README.kr.md +5 -6
  2. package/README.md +5 -6
  3. package/docs/kr/architecture/storage-model.md +1 -1
  4. package/docs/kr/architecture.md +8 -12
  5. package/docs/kr/cli.md +6 -6
  6. package/docs/kr/container.md +1 -1
  7. package/docs/kr/performance-improvement-plan-v2.md +2 -2
  8. package/docs/project-structure-overview.md +2 -2
  9. package/package.json +7 -2
  10. package/runtime/BUILD.json +2 -2
  11. package/runtime/bin/okstra-antigravity-exec.sh +1 -2
  12. package/runtime/bin/okstra-codex-exec.sh +1 -2
  13. package/runtime/bin/okstra-render-final-report.py +2 -9
  14. package/runtime/bin/okstra-spawn-followups.py +4 -6
  15. package/runtime/bin/okstra-trace-cleanup.sh +24 -4
  16. package/runtime/prompts/lead/okstra-lead-contract.md +2 -2
  17. package/runtime/prompts/lead/report-writer.md +1 -1
  18. package/runtime/prompts/profiles/_common-contract.md +9 -11
  19. package/runtime/python/okstra_ctl/codex_dispatch.py +4 -12
  20. package/runtime/python/okstra_ctl/consumers.py +35 -0
  21. package/runtime/python/okstra_ctl/container.py +4 -6
  22. package/runtime/python/okstra_ctl/dispatch_core.py +4 -12
  23. package/runtime/python/okstra_ctl/final_report_paths.py +24 -0
  24. package/runtime/python/okstra_ctl/handoff.py +2 -1
  25. package/runtime/python/okstra_ctl/implementation_stage.py +7 -11
  26. package/runtime/python/okstra_ctl/report_view_artifacts.py +14 -0
  27. package/runtime/python/okstra_ctl/report_views.py +7 -4
  28. package/runtime/python/okstra_ctl/run.py +3 -10
  29. package/runtime/python/okstra_ctl/stage_targets.py +18 -0
  30. package/runtime/python/okstra_ctl/wizard.py +24 -25
  31. package/runtime/python/okstra_ctl/wizard_stage_intent.py +54 -0
  32. package/runtime/python/okstra_token_usage/report.py +2 -5
  33. package/runtime/validators/validate-report-views.py +2 -1
  34. package/runtime/validators/validate-run.py +1 -11
  35. package/src/cli-registry.mjs +1 -1
  36. package/src/commands/lifecycle/install.mjs +10 -20
  37. package/src/commands/lifecycle/uninstall.mjs +7 -10
  38. package/docs/superpowers/plans/2026-05-12-ticket-id-in-reports.md +0 -638
  39. package/docs/superpowers/plans/2026-05-14-convergence-queue-pruning.md +0 -1568
  40. package/docs/superpowers/plans/2026-05-17-dual-format-final-report.md +0 -167
  41. package/docs/superpowers/plans/2026-05-20-final-report-language.md +0 -1501
  42. package/docs/superpowers/plans/2026-05-20-implementation-planning-multi-stage.md +0 -1267
  43. package/docs/superpowers/plans/2026-05-20-okstra-run-prompt-sot-b1.md +0 -1007
  44. package/docs/superpowers/plans/2026-05-20-wizard-messages-json-sot.md +0 -720
  45. package/docs/superpowers/plans/2026-05-20-wizard-prompt-json-sot-a1.md +0 -681
  46. package/docs/superpowers/plans/2026-05-21-improvement-discovery-task-type.md +0 -1691
  47. package/docs/superpowers/plans/2026-05-24-implementation-lead-context-slimming.md +0 -1700
  48. package/docs/superpowers/plans/2026-05-25-okstra-project-root-rename.md +0 -158
  49. package/docs/superpowers/plans/2026-05-26-wizard-3-option-picker.md +0 -860
  50. package/docs/superpowers/plans/2026-06-02-final-verification-protocol-hardening.md +0 -326
  51. package/docs/superpowers/plans/2026-06-02-okstra-run-branch-confirm-step.md +0 -337
  52. package/docs/superpowers/plans/2026-06-02-okstra-run-phase-pane-cleanup.md +0 -410
  53. package/docs/superpowers/plans/2026-06-02-requirements-discovery-fanout.md +0 -728
  54. package/docs/superpowers/plans/2026-06-04-adversarial-implementation-planning.md +0 -294
  55. package/docs/superpowers/plans/2026-06-04-adversarial-verification.md +0 -570
  56. package/docs/superpowers/plans/2026-06-04-coverage-critic.md +0 -516
  57. package/docs/superpowers/plans/2026-06-04-stage-cohesion-planner.md +0 -351
  58. package/docs/superpowers/plans/2026-06-04-stage-run-batching.md +0 -457
  59. package/docs/superpowers/plans/2026-06-05-acceptance-critic.md +0 -251
  60. package/docs/superpowers/plans/2026-06-05-compact-markdown-report-tables.md +0 -323
  61. package/docs/superpowers/plans/2026-06-05-wizard-batch-prompts.md +0 -559
  62. package/docs/superpowers/plans/2026-06-06-final-verification-whole-task-gate.md +0 -993
  63. package/docs/superpowers/plans/2026-06-06-stage-parallel-and-pending-fixes.md +0 -93
  64. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p1.md +0 -447
  65. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p2.md +0 -289
  66. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p3.md +0 -774
  67. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p4.md +0 -303
  68. package/docs/superpowers/plans/2026-06-06-stage-worktree-isolation-p5-multidep-base.md +0 -387
  69. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase2.md +0 -275
  70. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase3.md +0 -282
  71. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4a.md +0 -147
  72. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4b.md +0 -262
  73. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4c.md +0 -184
  74. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4d.md +0 -88
  75. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa-phase4e.md +0 -250
  76. package/docs/superpowers/plans/2026-06-07-stage-conformance-qa.md +0 -409
  77. package/docs/superpowers/plans/2026-06-09-implementation-run-artifact-stage-isolation.md +0 -320
  78. package/docs/superpowers/plans/2026-06-10-concurrent-run-team-guard.md +0 -456
  79. package/docs/superpowers/plans/2026-06-10-git-reconcile-stale-sha-recovery.md +0 -1408
  80. package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling-PROBE.md +0 -42
  81. package/docs/superpowers/plans/2026-06-10-lead-worker-completion-polling.md +0 -337
  82. package/docs/superpowers/plans/2026-06-10-p6-token-usage-incremental.md +0 -1029
  83. package/docs/superpowers/plans/2026-06-10-stage-group-handoff.md +0 -1572
  84. package/docs/superpowers/plans/2026-06-11-fix-cycle.md +0 -1290
  85. package/docs/superpowers/plans/2026-06-11-wizard-whole-task-final-verification.md +0 -526
  86. package/docs/superpowers/plans/2026-06-12-html-plan-approval.md +0 -1000
  87. package/docs/superpowers/plans/2026-06-13-repo-risk-hardening.md +0 -493
  88. package/docs/superpowers/plans/2026-06-14-host-runtime-auto-execution.md +0 -1775
  89. package/docs/superpowers/plans/2026-06-15-coding-preflight-pack-dispatch-path.md +0 -504
  90. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-final-fixups.md +0 -342
  91. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-fixups.md +0 -258
  92. package/docs/superpowers/plans/2026-06-15-internal-skill-migration-remaining-fixups.md +0 -387
  93. package/docs/superpowers/plans/2026-06-15-internal-skill-resource-migration.md +0 -749
  94. package/docs/superpowers/plans/2026-06-15-worker-prompt-anchor-final-fixups.md +0 -828
  95. package/docs/superpowers/plans/2026-06-15-worker-prompt-header-error-contract.md +0 -490
  96. package/docs/superpowers/plans/2026-06-17-okstra-error-report.md +0 -724
  97. package/docs/superpowers/plans/2026-06-18-subagent-pane-reclaim.md +0 -483
  98. package/docs/superpowers/plans/2026-06-19-agent-operating-standard.md +0 -476
  99. package/docs/superpowers/plans/2026-06-19-okstra-inspect-recap-facet.md +0 -647
  100. package/docs/superpowers/plans/2026-06-20-error-feedback-loop.md +0 -1019
  101. package/docs/superpowers/plans/2026-06-20-run-index-row-normalization.md +0 -583
  102. package/docs/superpowers/plans/2026-06-20-stage-auto-integrate-teardown.md +0 -819
  103. package/docs/superpowers/plans/2026-06-21-okstra-container-local-user-test.md +0 -714
  104. package/docs/superpowers/plans/2026-06-21-release-handoff-local-checkout.md +0 -348
  105. package/docs/superpowers/plans/2026-06-23-implementation-stage-multiselect-chain.md +0 -630
  106. package/docs/superpowers/plans/2026-06-23-implementation-step-commit-granularity.md +0 -274
  107. package/docs/superpowers/plans/2026-06-23-phase-batch-cleanup-enforcement.md +0 -409
  108. package/docs/superpowers/plans/2026-06-24-cross-project-precondition.md +0 -542
  109. package/docs/superpowers/plans/2026-06-24-decision-drafts-render-gap.md +0 -568
  110. package/docs/superpowers/plans/2026-06-24-manual-user-test-section.md +0 -203
  111. package/docs/superpowers/plans/2026-06-24-okstra-container-build-rename.md +0 -525
  112. package/docs/superpowers/specs/2026-05-12-ticket-id-in-reports-design.md +0 -131
  113. package/docs/superpowers/specs/2026-05-15-implementation-plan-verification-design.md +0 -254
  114. package/docs/superpowers/specs/2026-05-20-final-report-language-design.md +0 -383
  115. package/docs/superpowers/specs/2026-05-20-implementation-planning-multi-stage-design.md +0 -322
  116. package/docs/superpowers/specs/2026-05-20-okstra-run-prompt-sot-design.md +0 -299
  117. package/docs/superpowers/specs/2026-05-21-improvement-discovery-task-type-design.md +0 -335
  118. package/docs/superpowers/specs/2026-06-02-okstra-run-branch-confirm-step-design.md +0 -113
  119. package/docs/superpowers/specs/2026-06-02-okstra-run-phase-pane-cleanup-design.md +0 -173
  120. package/docs/superpowers/specs/2026-06-02-requirements-discovery-fanout-design.md +0 -154
  121. package/docs/superpowers/specs/2026-06-04-adversarial-implementation-planning-design.md +0 -90
  122. package/docs/superpowers/specs/2026-06-04-adversarial-verification-design.md +0 -176
  123. package/docs/superpowers/specs/2026-06-04-coverage-critic-design.md +0 -99
  124. package/docs/superpowers/specs/2026-06-04-stage-splitting-cost-aware-design.md +0 -98
  125. package/docs/superpowers/specs/2026-06-05-acceptance-critic-design.md +0 -90
  126. package/docs/superpowers/specs/2026-06-05-compact-markdown-report-tables-design.md +0 -87
  127. package/docs/superpowers/specs/2026-06-05-wizard-batch-prompts-design.md +0 -121
  128. package/docs/superpowers/specs/2026-06-06-final-verification-whole-task-gate-design.md +0 -126
  129. package/docs/superpowers/specs/2026-06-06-stage-worktree-isolation-design.md +0 -180
  130. package/docs/superpowers/specs/2026-06-06-vertical-slice-tdd-planning-design.md +0 -179
  131. package/docs/superpowers/specs/2026-06-07-stage-conformance-qa-design.md +0 -169
  132. package/docs/superpowers/specs/2026-06-09-executor-model-custom-id-cascade-design.md +0 -66
  133. package/docs/superpowers/specs/2026-06-09-implementation-run-artifact-stage-isolation-design.md +0 -87
  134. package/docs/superpowers/specs/2026-06-10-blocking-contract-posthoc-conformance-design.md +0 -168
  135. package/docs/superpowers/specs/2026-06-10-concurrent-run-team-guard-design.md +0 -107
  136. package/docs/superpowers/specs/2026-06-10-git-reconcile-stale-sha-recovery-design.md +0 -105
  137. package/docs/superpowers/specs/2026-06-10-lead-worker-completion-polling-design.md +0 -113
  138. package/docs/superpowers/specs/2026-06-10-stage-group-handoff-design.md +0 -156
  139. package/docs/superpowers/specs/2026-06-11-brief-entry-only-handoff-stage-entry-design.md +0 -158
  140. package/docs/superpowers/specs/2026-06-11-fix-cycle-design.md +0 -94
  141. package/docs/superpowers/specs/2026-06-11-wizard-whole-task-final-verification-design.md +0 -89
  142. package/docs/superpowers/specs/2026-06-12-codex-lead-adapter-design.md +0 -358
  143. package/docs/superpowers/specs/2026-06-12-html-plan-approval-design.md +0 -85
  144. package/docs/superpowers/specs/2026-06-12-stage-discipline-rule-design.md +0 -108
  145. package/docs/superpowers/specs/2026-06-13-forbidden-actions-ssot-design.md +0 -134
  146. package/docs/superpowers/specs/2026-06-13-neutral-tmux-lead-adapter-design.md +0 -284
  147. package/docs/superpowers/specs/2026-06-14-host-runtime-auto-execution-design.md +0 -404
  148. package/docs/superpowers/specs/2026-06-17-okstra-error-report-design.md +0 -123
  149. package/docs/superpowers/specs/2026-06-18-subagent-pane-reclaim-design.md +0 -150
  150. package/docs/superpowers/specs/2026-06-19-agent-operating-standard-design.md +0 -120
  151. package/docs/superpowers/specs/2026-06-19-okstra-inspect-recap-facet-design.md +0 -96
  152. package/docs/superpowers/specs/2026-06-20-error-feedback-loop-design.md +0 -171
  153. package/docs/superpowers/specs/2026-06-20-run-index-row-normalization-design.md +0 -112
  154. package/docs/superpowers/specs/2026-06-20-stage-auto-integrate-teardown-design.md +0 -145
  155. package/docs/superpowers/specs/2026-06-21-okstra-container-local-user-test-design.md +0 -125
  156. package/docs/superpowers/specs/2026-06-21-release-handoff-local-checkout-design.md +0 -113
  157. package/docs/superpowers/specs/2026-06-23-implementation-stage-multiselect-chain-design.md +0 -138
  158. package/docs/superpowers/specs/2026-06-23-implementation-step-commit-granularity-design.md +0 -120
  159. package/docs/superpowers/specs/2026-06-24-cross-project-precondition-design.md +0 -129
  160. package/docs/superpowers/specs/2026-06-24-decision-drafts-render-gap-design.md +0 -185
  161. package/docs/superpowers/specs/2026-06-24-manual-user-test-section-design.md +0 -121
  162. package/docs/superpowers/specs/2026-06-24-okstra-container-build-rename-design.md +0 -110
@@ -18,6 +18,10 @@ from datetime import datetime, timezone
18
18
  from pathlib import Path
19
19
  from typing import Any, Mapping, Sequence
20
20
 
21
+ from .final_report_paths import (
22
+ final_report_data_path as _final_report_data_path,
23
+ final_report_markdown_path as _final_report_markdown_path,
24
+ )
21
25
  from .lead_events import LeadEvent, append_lead_event
22
26
  from .wrapper_status import status_path_for_prompt
23
27
  from .paths import task_dir
@@ -1450,18 +1454,6 @@ def _failure_reason(exit_code: int, missing_paths: Sequence[Path]) -> str:
1450
1454
  return "worker dispatch failed"
1451
1455
 
1452
1456
 
1453
- def _final_report_data_path(markdown_path: Path) -> Path:
1454
- if markdown_path.name.endswith(".md"):
1455
- return markdown_path.with_name(markdown_path.name[:-3] + ".data.json")
1456
- return markdown_path.with_suffix(".data.json")
1457
-
1458
-
1459
- def _final_report_markdown_path(data_json_path: Path) -> Path:
1460
- if data_json_path.name.endswith(".data.json"):
1461
- return data_json_path.with_name(data_json_path.name[:-10] + ".md")
1462
- return data_json_path.with_suffix(".md")
1463
-
1464
-
1465
1457
  def _replace_report_writer_model_line(
1466
1458
  prompt_path: Path,
1467
1459
  model_execution_value: str,
@@ -9,6 +9,7 @@ from __future__ import annotations
9
9
 
10
10
  import json
11
11
  import re
12
+ from dataclasses import dataclass
12
13
  from pathlib import Path
13
14
  from typing import Any, Dict, List, Optional
14
15
 
@@ -17,6 +18,17 @@ from .run_context import consumers_mutex
17
18
  CONSUMERS_FILENAME = "consumers.jsonl"
18
19
 
19
20
 
21
+ @dataclass(frozen=True)
22
+ class StageConsumerState:
23
+ rows: List[Dict[str, Any]]
24
+ done_rows: List[Dict[str, Any]]
25
+ done_by_stage: Dict[int, Dict[str, Any]]
26
+ done_stages: set[int]
27
+ started_stages: set[int]
28
+ verified_accepted_stages: set[int]
29
+ pr_covered_stages: set[int]
30
+
31
+
20
32
  def _path(plan_run_root: Path) -> Path:
21
33
  return plan_run_root / CONSUMERS_FILENAME
22
34
 
@@ -50,6 +62,29 @@ def latest_done_by_stage(rows: List[Dict[str, Any]]) -> Dict[int, Dict[str, Any]
50
62
  return out
51
63
 
52
64
 
65
+ def read_stage_consumer_state(
66
+ plan_run_root: Path, *, recover_from_carry: bool = False,
67
+ ) -> StageConsumerState:
68
+ if recover_from_carry:
69
+ backfill_done_from_carry(plan_run_root)
70
+ rows = read_consumers(plan_run_root)
71
+ done_rows = [r for r in rows if r.get("status") == "done"]
72
+ done_by_stage = latest_done_by_stage(rows)
73
+ started = {
74
+ r["stage"] for r in rows
75
+ if r.get("status") == "started" and isinstance(r.get("stage"), int)
76
+ }
77
+ return StageConsumerState(
78
+ rows=rows,
79
+ done_rows=done_rows,
80
+ done_by_stage=done_by_stage,
81
+ done_stages=set(done_by_stage.keys()),
82
+ started_stages=started,
83
+ verified_accepted_stages=verified_accepted_stages(rows),
84
+ pr_covered_stages=pr_covered_stages(rows),
85
+ )
86
+
87
+
53
88
  def append_consumer(plan_run_root: Path, *, impl_task_key: str, stage: int,
54
89
  status: str, force_reappend: bool = False,
55
90
  **fields: Any) -> None:
@@ -506,7 +506,7 @@ def _split_task_key(task_key: str) -> tuple[str, str, str]:
506
506
  def _resolve_up_inputs(project_root: Path, task_key: str) -> dict:
507
507
  """task-key 로 up 에 필요한 입력(worktree/stage_map/done_rows/anchor)을 모은다."""
508
508
  from . import worktree_registry as _reg
509
- from .consumers import read_consumers, backfill_done_from_carry
509
+ from .consumers import read_stage_consumer_state
510
510
  from .plan_run_root import resolve_plan_run_root_by_task_key
511
511
 
512
512
  project_id, group, task_id = _split_task_key(task_key)
@@ -517,11 +517,9 @@ def _resolve_up_inputs(project_root: Path, task_key: str) -> dict:
517
517
  "없습니다. 먼저 해당 task 의 phase 를 한 번 실행해 worktree 를 만드세요.")
518
518
  plan = resolve_plan_run_root_by_task_key(
519
519
  project_root=project_root, task_group=group, task_id=task_id)
520
- # carry sidecar 가 stage 완료의 SSOT 다 — final-verification 과 동일하게 누락된
521
- # done 행을 carry 에서 복구한 뒤 읽어, 두 경로가 같은 디스크 상태에서 갈라지지
522
- # 않게 한다(crash/부분쓰기로 consumers.jsonl done 행만 유실된 경우 대비).
523
- backfill_done_from_carry(plan.run_root)
524
- done_rows = [r for r in read_consumers(plan.run_root) if r.get("status") == "done"]
520
+ done_rows = read_stage_consumer_state(
521
+ plan.run_root, recover_from_carry=True,
522
+ ).done_rows
525
523
  stage_map = _plan_stage_map(plan.approved_plan_path)
526
524
  anchor = _reg.get_implementation_base(project_id, group, task_id) or ""
527
525
  return {
@@ -11,6 +11,10 @@ from pathlib import Path
11
11
  from typing import Any, Mapping, Sequence
12
12
 
13
13
  from . import tmux
14
+ from .final_report_paths import (
15
+ final_report_data_path as _final_report_data_path,
16
+ final_report_markdown_path as _final_report_markdown_path,
17
+ )
14
18
  from .lead_events import LeadEvent, append_lead_event
15
19
  from .worker_prompt_headers import WorkerPromptHeaderError, worker_prompt_headers
16
20
  from .wrapper_status import read_wrapper_status, status_path_for_prompt
@@ -823,18 +827,6 @@ def _completion_paths(worker_id: str, result_path: Path, manifest: Mapping[str,
823
827
  return (data_json, _final_report_markdown_path(data_json), result_path)
824
828
 
825
829
 
826
- def _final_report_data_path(markdown_path: Path) -> Path:
827
- if markdown_path.name.endswith(".md"):
828
- return markdown_path.with_name(markdown_path.name[:-3] + ".data.json")
829
- return markdown_path.with_suffix(".data.json")
830
-
831
-
832
- def _final_report_markdown_path(data_json_path: Path) -> Path:
833
- if data_json_path.name.endswith(".data.json"):
834
- return data_json_path.with_name(data_json_path.name[:-10] + ".md")
835
- return data_json_path.with_suffix(".md")
836
-
837
-
838
830
  def _run_dir(plan: DispatchPlan) -> Path:
839
831
  value = _string_value(plan.manifest.get("runDirectoryPath"))
840
832
  return _resolve_project_path(plan.project_root, value) if value else plan.team_state_path.parent.parent
@@ -0,0 +1,24 @@
1
+ """Path helpers for the final-report markdown/data.json pair."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+
7
+ DATA_JSON_SUFFIX = ".data.json"
8
+ MARKDOWN_SUFFIX = ".md"
9
+
10
+
11
+ def final_report_data_path(report_path: Path) -> Path:
12
+ """Return the data.json sibling for a final-report markdown path."""
13
+ name = report_path.name
14
+ if name.endswith(MARKDOWN_SUFFIX):
15
+ return report_path.with_name(name[: -len(MARKDOWN_SUFFIX)] + DATA_JSON_SUFFIX)
16
+ return report_path.with_suffix(DATA_JSON_SUFFIX)
17
+
18
+
19
+ def final_report_markdown_path(data_path: Path) -> Path:
20
+ """Return the markdown sibling for a final-report data.json path."""
21
+ name = data_path.name
22
+ if name.endswith(DATA_JSON_SUFFIX):
23
+ return data_path.with_name(name[: -len(DATA_JSON_SUFFIX)] + MARKDOWN_SUFFIX)
24
+ return data_path.with_suffix(MARKDOWN_SUFFIX)
@@ -13,6 +13,7 @@ from pathlib import Path
13
13
  from typing import Any, Callable, Dict, List, Optional, Tuple
14
14
 
15
15
  from . import consumers, worktree_registry
16
+ from .final_report_paths import final_report_markdown_path
16
17
  from .worktree import (compute_branch_name, compute_worktree_path,
17
18
  main_worktree_path, is_dirty_excluding_okstra,
18
19
  nested_worktree_excludes, is_ancestor, merge_branch,
@@ -91,7 +92,7 @@ def latest_whole_task_fv_accepted(project_root, project_id: str,
91
92
  if ((data.get("header") or {}).get("taskType") == "final-verification"
92
93
  and data.get("verificationScope") == "whole-task"
93
94
  and token == "accepted"):
94
- md = dj.with_name(dj.name[:-len(".data.json")] + ".md")
95
+ md = final_report_markdown_path(dj)
95
96
  return str(md if md.is_file() else dj)
96
97
  return ""
97
98
 
@@ -66,16 +66,14 @@ def select_and_provision_implementation_stage(
66
66
  """
67
67
  del task_key # The registry uses the split identity fields.
68
68
 
69
- from .consumers import read_consumers, backfill_done_from_carry
69
+ from .consumers import backfill_done_from_carry, read_stage_consumer_state
70
70
  from . import worktree as _worktree
71
71
  from . import worktree_registry as _reg
72
72
 
73
73
  plan_run_root = Path(inp.approved_plan_path).resolve().parents[1]
74
74
  backfill_done_from_carry(plan_run_root)
75
75
  auto_reconcile_best_effort(inp, plan_run_root)
76
- consumed = read_consumers(plan_run_root)
77
- done_stages = {r["stage"] for r in consumed if r.get("status") == "done"}
78
- started_stages = {r["stage"] for r in consumed if r.get("status") == "started"}
76
+ consumers = read_stage_consumer_state(plan_run_root)
79
77
  reserved_stages = _reg.list_active_stage_numbers(
80
78
  inp.project_id,
81
79
  inp.task_group,
@@ -83,18 +81,17 @@ def select_and_provision_implementation_stage(
83
81
  )
84
82
 
85
83
  try:
86
- batch = stage_targets.resolve_effective_stages(
84
+ selected = stage_targets.resolve_implementation_stage(
87
85
  ctx_stage_map,
88
- done_stages,
86
+ consumers.done_stages,
89
87
  inp.stage,
90
- started_stages=started_stages,
88
+ started_stages=consumers.started_stages,
91
89
  reserved_stages=reserved_stages,
92
90
  )
93
91
  except stage_targets.StageTargetError as exc:
94
92
  raise _as_implementation_stage_error(exc) from exc
95
93
 
96
- selected = batch[0]
97
- concurrent_stages = sorted(reserved_stages - done_stages - {selected})
94
+ concurrent_stages = sorted(reserved_stages - consumers.done_stages - {selected})
98
95
 
99
96
  if executor_worktree_status.startswith("skipped"):
100
97
  head = _git_out(inp.project_root, "rev-parse", "HEAD")
@@ -133,11 +130,10 @@ def select_and_provision_implementation_stage(
133
130
  selected_stage = next(
134
131
  s for s in ctx_stage_map if s["stage_number"] == selected
135
132
  )
136
- consumer_done_rows = [r for r in consumed if r.get("status") == "done"]
137
133
  try:
138
134
  stage_base = stage_targets.resolve_stage_base_commit(
139
135
  selected_stage,
140
- consumer_done_rows,
136
+ consumers.done_rows,
141
137
  anchor_base_commit=anchor,
142
138
  candidate_base=head_sha,
143
139
  project_root=Path(task_worktree_path),
@@ -0,0 +1,14 @@
1
+ """Path helpers for generated final-report view artifacts."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+
7
+ def html_view_path(report_path: Path) -> Path:
8
+ """Return the self-contained HTML view path for a final-report markdown."""
9
+ return report_path.with_suffix(".html")
10
+
11
+
12
+ def user_responses_dir_for_report(report_path: Path) -> Path:
13
+ """Return the sidecar directory used by the report's exported responses."""
14
+ return report_path.parent.parent / "user-responses"
@@ -28,6 +28,9 @@ from dataclasses import dataclass
28
28
  from pathlib import Path
29
29
  from typing import Iterable, Optional
30
30
 
31
+ from .final_report_paths import final_report_data_path
32
+ from .report_view_artifacts import html_view_path, user_responses_dir_for_report
33
+
31
34
 
32
35
  def source_digest(src_md: str) -> str:
33
36
  """Stable identifier for a final-report markdown body. The HTML
@@ -904,7 +907,7 @@ def plan_approval_context(
904
907
  validator 가 같은 판정을 공유한다. recommendedOption 이 비거나 후보에 없으면
905
908
  첫 후보로 폴백한다 — 항상 정확히 한 옵션이 selected 가 되어 브라우저 자동선택분의
906
909
  묵시 Export 를 차단한다."""
907
- data_path = src_md_path.with_name(src_md_path.stem + ".data.json")
910
+ data_path = final_report_data_path(src_md_path)
908
911
  if not data_path.is_file():
909
912
  return None
910
913
  try:
@@ -1020,14 +1023,14 @@ def render_html_view(
1020
1023
  css: str,
1021
1024
  js: str,
1022
1025
  ) -> Path | None:
1023
- """Write ``<stem>.html`` next to ``src_md_path`` and return its path,
1026
+ """Write the HTML view artifact for ``src_md_path`` and return its path,
1024
1027
  or return ``None`` when the report needs no interactive view — §1
1025
1028
  clarification rows 도 없고 Plan Approval 위젯 대상도 아닐 때
1026
1029
  (``report_has_clarification_items`` / ``plan_approval_context``).
1027
1030
  Idempotent — overwrites an existing html sibling, and removes a stale
1028
1031
  one when the report no longer needs a view."""
1029
1032
  src_text = src_md_path.read_text(encoding="utf-8")
1030
- html_path = src_md_path.with_name(src_md_path.stem + ".html")
1033
+ html_path = html_view_path(src_md_path)
1031
1034
  # Fail-closed on §1 heading drift: when a `## 1. Clarification Items`
1032
1035
  # heading exists but its strict form does not parse,
1033
1036
  # `report_has_clarification_items` returns False (lenient parse), so a
@@ -1052,5 +1055,5 @@ def render_html_view(
1052
1055
  html_path.write_text(html_text, encoding="utf-8")
1053
1056
  # 사용자 확인(폼/승인)이 필요한 보고서 — Export 파일의 저장 위치를 미리
1054
1057
  # 만들어 사용자가 디렉토리를 만들 필요가 없게 한다 (reports/ 의 sibling).
1055
- (src_md_path.parent.parent / "user-responses").mkdir(parents=True, exist_ok=True)
1058
+ user_responses_dir_for_report(src_md_path).mkdir(parents=True, exist_ok=True)
1056
1059
  return html_path
@@ -43,6 +43,7 @@ from .material import (
43
43
  resolve_related_tasks,
44
44
  )
45
45
  from .final_report_schema import load_schema
46
+ from .final_report_paths import final_report_data_path as _final_report_data_path
46
47
  from .lead_events import LeadEvent, append_lead_event
47
48
  from .lead_runtime import ALLOWED_LEAD_RUNTIMES
48
49
  from .models import ModelAssignment, resolve_model_metadata
@@ -146,13 +147,6 @@ def _extract_frontmatter_block(body: str) -> str | None:
146
147
  return m.group(1) if m else None
147
148
 
148
149
 
149
- def _final_report_data_path(report_path: Path) -> Path:
150
- name = report_path.name
151
- if name.endswith(".md"):
152
- return report_path.with_name(name[:-3] + ".data.json")
153
- return report_path.with_suffix(".data.json")
154
-
155
-
156
150
  def _load_final_report_data_if_present(report_path: Path) -> tuple[Path, dict] | None:
157
151
  data_path = _final_report_data_path(report_path)
158
152
  if not data_path.is_file():
@@ -1514,7 +1508,7 @@ def _reserve_final_verification_target(
1514
1508
  """final-verification 의 검증 target 을 registry/consumers/git 에서
1515
1509
  해소하고 gate 를 강제한다. 위반 시 PrepareError. 결과를 ctx 의
1516
1510
  VERIFICATION_* 키로 주입한다."""
1517
- from .consumers import read_consumers, backfill_done_from_carry
1511
+ from .consumers import backfill_done_from_carry, read_stage_consumer_state
1518
1512
  from . import worktree_registry as _reg
1519
1513
 
1520
1514
  plan_run_root = plan_run_root_from_approved_plan(inp.approved_plan_path)
@@ -1522,8 +1516,7 @@ def _reserve_final_verification_target(
1522
1516
  # rows before the whole-task gate checks every stage.
1523
1517
  backfill_done_from_carry(plan_run_root)
1524
1518
  _auto_reconcile_best_effort(inp, plan_run_root)
1525
- done_rows = [r for r in read_consumers(plan_run_root)
1526
- if r.get("status") == "done"]
1519
+ done_rows = read_stage_consumer_state(plan_run_root).done_rows
1527
1520
 
1528
1521
  if inp.stage and inp.stage != "auto":
1529
1522
  n = int(inp.stage)
@@ -98,6 +98,24 @@ def resolve_effective_stages(
98
98
  return batch
99
99
 
100
100
 
101
+ def resolve_implementation_stage(
102
+ stages: list[dict[str, Any]],
103
+ done_stages: set[int],
104
+ requested: str,
105
+ *,
106
+ started_stages: set[int] | None = None,
107
+ reserved_stages: set[int] | None = None,
108
+ ) -> int:
109
+ """Return the single Stage Map stage selected for one implementation run."""
110
+ return resolve_effective_stages(
111
+ stages,
112
+ done_stages,
113
+ requested,
114
+ started_stages=started_stages,
115
+ reserved_stages=reserved_stages,
116
+ )[0]
117
+
118
+
101
119
  def order_stage_closure(
102
120
  stages, selected: set[int], done: set[int],
103
121
  ) -> list[int]:
@@ -34,6 +34,7 @@ from okstra_ctl.models import (
34
34
  resolve_model_metadata,
35
35
  )
36
36
  from okstra_ctl.clarification_items import scan_approval_gate, user_response_sidecars
37
+ from okstra_ctl.final_report_paths import final_report_data_path
37
38
  from okstra_ctl.pr_template import PrTemplateError, resolve_pr_template_path
38
39
  from okstra_ctl.run import (
39
40
  APPROVED_FRONTMATTER_PATTERN,
@@ -60,6 +61,12 @@ from okstra_ctl.workers import (
60
61
  validate_workers_against_profile,
61
62
  )
62
63
  from okstra_ctl.workflow import PHASE_SEQUENCE
64
+ from okstra_ctl.wizard_stage_intent import (
65
+ WHOLE_TASK_STAGE,
66
+ WizardStageIntentError,
67
+ resolve_wizard_stage_intent,
68
+ wizard_stage_confirmation_label,
69
+ )
63
70
  from okstra_ctl import fix_cycles, worktree_registry
64
71
  from okstra_ctl.worktree import (
65
72
  compute_worktree_path,
@@ -607,7 +614,7 @@ def _find_html_approval_sidecar(
607
614
  def _validate_sidecar_option(plan_path: Path, option_name: str, errors_t: dict) -> None:
608
615
  """sidecar 의 옵션 이름이 plan data.json 의 optionCandidates 에 있는지
609
616
  검증한다. 없으면 유효 후보를 나열하며 거부한다 (fail-closed)."""
610
- data_path = plan_path.with_name(plan_path.name[: -len(".md")] + ".data.json")
617
+ data_path = final_report_data_path(plan_path)
611
618
  candidates: list[str] = []
612
619
  if data_path.is_file():
613
620
  try:
@@ -1652,7 +1659,6 @@ _REUSE_LAST_TOKEN = "__reuse_last__"
1652
1659
  _SIBLINGS_TOKEN = "__siblings__"
1653
1660
  _LATEST_REPORT_TOKEN = "__latest_report__"
1654
1661
  _PROJECT_DEFAULT_TOKEN = "__project_default__"
1655
- WHOLE_TASK_STAGE = "__whole_task__"
1656
1662
  ALL_STAGES = "__all_stages__"
1657
1663
 
1658
1664
 
@@ -3488,21 +3494,14 @@ def render_args(state: WizardState) -> dict[str, str]:
3488
3494
  if state.reuse_worktree or state.task_type == "final-verification"
3489
3495
  else state.base_ref
3490
3496
  )
3491
- chain_stages = ""
3492
- if state.task_type == "implementation":
3493
- stage = state.selected_stage or "auto"
3494
- chain_stages = state.selected_stages or stage
3495
- elif state.task_type == "final-verification":
3496
- if state.selected_stage == WHOLE_TASK_STAGE:
3497
- stage = "" # prepare 가 빈 stage 를 whole-task 로 해석
3498
- elif not state.selected_stage or state.selected_stage == "auto":
3499
- raise WizardError(
3500
- "final-verification requires an explicit stage number"
3501
- )
3502
- else:
3503
- stage = state.selected_stage
3504
- else:
3505
- stage = ""
3497
+ try:
3498
+ stage_intent = resolve_wizard_stage_intent(
3499
+ task_type=state.task_type,
3500
+ selected_stage=state.selected_stage,
3501
+ selected_stages=state.selected_stages,
3502
+ )
3503
+ except WizardStageIntentError as exc:
3504
+ raise WizardError(str(exc)) from exc
3506
3505
  pr_template = (
3507
3506
  state.pr_template_path
3508
3507
  if state.task_type == "release-handoff"
@@ -3518,9 +3517,9 @@ def render_args(state: WizardState) -> dict[str, str]:
3518
3517
  "executor": state.executor,
3519
3518
  "critic": state.critic,
3520
3519
  "approved-plan": state.approved_plan_path,
3521
- "stage": stage,
3520
+ "stage": stage_intent.stage,
3522
3521
  "stages": state.handoff_stages,
3523
- "chain-stages": chain_stages,
3522
+ "chain-stages": stage_intent.chain_stages,
3524
3523
  "base-ref": base_ref,
3525
3524
  "workers": workers,
3526
3525
  "directive": state.directive,
@@ -3581,12 +3580,12 @@ def confirmation_block(state: WizardState) -> str:
3581
3580
  lines.append(f" critic : {state.critic or '(off)'}")
3582
3581
  if state.task_type in _STAGE_SCOPED_TASK_TYPES:
3583
3582
  lines.append(f" approved-plan : {state.approved_plan_path}")
3584
- stage = (
3585
- _msg(state.workspace_root, "confirmation", "stage_whole_task")
3586
- if state.selected_stage == WHOLE_TASK_STAGE
3587
- else (state.selected_stage
3588
- or ("auto" if state.task_type == "implementation"
3589
- else "(not selected)"))
3583
+ stage = wizard_stage_confirmation_label(
3584
+ task_type=state.task_type,
3585
+ selected_stage=state.selected_stage,
3586
+ whole_task_label=_msg(
3587
+ state.workspace_root, "confirmation", "stage_whole_task"
3588
+ ),
3590
3589
  )
3591
3590
  lines.append(f" stage : {stage}")
3592
3591
  if state.clarification_response_path:
@@ -0,0 +1,54 @@
1
+ """Stage-related intent projection for okstra-run wizard output."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass
5
+
6
+
7
+ WHOLE_TASK_STAGE = "__whole_task__"
8
+
9
+
10
+ class WizardStageIntentError(ValueError):
11
+ """Raised when wizard state cannot produce stage-related render args."""
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class WizardStageIntent:
16
+ stage: str
17
+ chain_stages: str
18
+
19
+
20
+ def resolve_wizard_stage_intent(
21
+ *,
22
+ task_type: str,
23
+ selected_stage: str,
24
+ selected_stages: str,
25
+ ) -> WizardStageIntent:
26
+ """Return the stage-related ``okstra render-bundle`` arguments."""
27
+ if task_type == "implementation":
28
+ stage = selected_stage or "auto"
29
+ return WizardStageIntent(stage=stage, chain_stages=selected_stages or stage)
30
+
31
+ if task_type == "final-verification":
32
+ if selected_stage == WHOLE_TASK_STAGE:
33
+ return WizardStageIntent(stage="", chain_stages="")
34
+ if not selected_stage or selected_stage == "auto":
35
+ raise WizardStageIntentError(
36
+ "final-verification requires an explicit stage number"
37
+ )
38
+ return WizardStageIntent(stage=selected_stage, chain_stages="")
39
+
40
+ return WizardStageIntent(stage="", chain_stages="")
41
+
42
+
43
+ def wizard_stage_confirmation_label(
44
+ *,
45
+ task_type: str,
46
+ selected_stage: str,
47
+ whole_task_label: str,
48
+ ) -> str:
49
+ """Return the human-readable stage value for the confirmation block."""
50
+ if selected_stage == WHOLE_TASK_STAGE:
51
+ return whole_task_label
52
+ if selected_stage:
53
+ return selected_stage
54
+ return "auto" if task_type == "implementation" else "(not selected)"
@@ -14,6 +14,7 @@ _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.final_report_paths import final_report_markdown_path # noqa: E402
17
18
  from okstra_ctl.render_final_report import ( # noqa: E402
18
19
  FinalReportRenderError,
19
20
  render_to_file,
@@ -209,11 +210,7 @@ def populate_and_render(
209
210
  changes = populate_data_token_cells(data_path, team_state)
210
211
 
211
212
  if output_path is None:
212
- name = data_path.name
213
- if name.endswith(".data.json"):
214
- output_path = data_path.with_name(name[: -len(".data.json")] + ".md")
215
- else:
216
- output_path = data_path.with_suffix(".md")
213
+ output_path = final_report_markdown_path(data_path)
217
214
 
218
215
  try:
219
216
  bytes_written = render_to_file(data_path, output_path)
@@ -38,6 +38,7 @@ from okstra_ctl.report_views import ( # noqa: E402
38
38
  plan_approval_context,
39
39
  source_digest,
40
40
  )
41
+ from okstra_ctl.report_view_artifacts import html_view_path # noqa: E402
41
42
 
42
43
 
43
44
  _EXTERNAL_URL_RE = re.compile(
@@ -83,7 +84,7 @@ def validate(report_path: Path) -> list[str]:
83
84
  return [f"final-report not found: {report_path}"]
84
85
 
85
86
  md = report_path.read_text(encoding="utf-8")
86
- html_path = report_path.with_name(report_path.stem + ".html")
87
+ html_path = html_view_path(report_path)
87
88
  # §1 헤딩이 있는데 파싱이 실패하면 md_ids 가 빈 []이 되어 "clarification 없음
88
89
  # → skip" 으로 흘러 HTML form parity 게이트가 조용히 열린다. fail-closed.
89
90
  if section_1_present_but_unparsed(md):
@@ -44,6 +44,7 @@ from okstra_ctl.md_table import ( # noqa: E402
44
44
  is_separator_row as _is_markdown_separator,
45
45
  split_pipe_row as _split_pipe_row,
46
46
  )
47
+ from okstra_ctl.final_report_paths import final_report_data_path as _data_path_for # noqa: E402
47
48
 
48
49
  TERMINAL_STATUSES = {"completed", "timeout", "error", "not-run"}
49
50
  ATTEMPTED_STATUSES = {"completed", "timeout", "error"}
@@ -1414,17 +1415,6 @@ def _validate_verdict_card_consistency(content: str, failures: list[str]) -> Non
1414
1415
  )
1415
1416
 
1416
1417
 
1417
- def _data_path_for(report_path: Path) -> Path:
1418
- """Derive the final-report data.json sibling from the markdown path.
1419
- ``foo.md`` → ``foo.data.json``. The data.json is the canonical JSON
1420
- SSOT; the markdown is rendered from it.
1421
- """
1422
- name = report_path.name
1423
- if name.endswith(".md"):
1424
- return report_path.with_name(name[:-3] + ".data.json")
1425
- return report_path.with_suffix(".data.json")
1426
-
1427
-
1428
1418
  def _load_final_report_data(report_path: Path) -> dict:
1429
1419
  """Best-effort parse of the final-report data.json sibling. Returns {} when
1430
1420
  absent or unparseable — those conditions are already surfaced as failures by
@@ -6,7 +6,7 @@ export const COMMAND_REGISTRY = [
6
6
  module: "./commands/lifecycle/install.mjs",
7
7
  export: "runInstall",
8
8
  category: "admin",
9
- summary: ["Install runtime; seed skills into existing agent homes"],
9
+ summary: ["Install runtime; seed skills into Agent and Claude homes"],
10
10
  },
11
11
  {
12
12
  name: "ensure-installed",